订单重复提交_springboot + aop + redis + 注解 , 实现接口幂等性校验

一、概念

幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次
比如:

  • 订单接口, 不能多次创建订单
  • 支付接口, 重复支付同一笔订单只能扣一次钱
  • 支付宝回调接口, 可能会多次回调, 必须处理重复回调
  • 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次
    等等

二、常见解决方案

  1. 唯一索引 -- 防止新增脏数据
  2. token机制 -- 防止页面重复提交
  3. 悲观锁 -- 获取数据的时候加锁(锁表或锁行)
  4. 乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据
  5. 分布式锁 -- redis(jedis、redisson)或zookeeper实现
  6. 状态机 -- 状态变更, 更新数据时判断状态

三、本文实现

本文采用第2种方式实现, 即通过redis + token机制实现接口幂等性校验

四、实现思路

本文主要处理场景:同一个用户,一个请求,在规定的时间内只能发起1次请求。

这边主要处理的防止页面重复提交,为保证幂等性,请求接口时,后端通过header或者接口请求参数获取登录信息+请求路径判断redis中是否存在此key。

  • 如果不存在, 正常处理业务逻辑, 并把此key存入redis中并设置过期时间, 那么, 如果是重复请求, 由于key已存在于redis, 则不能通过校验, 返回请勿重复操作提示
  • 如果存在, 说明在redis里的key还未过期,当前是重复请求, 返回提示即可

五、项目简介

  • springboot
  • redis 
  • @ReSubmitCheck注解 + AOP切面对请求进行拦截
  • @SharException全局异常处理
  • 压测工具: jmeter

说明:

  • 本文重点介绍幂等性核心实现, 关于springboot如何集成redisServerResponseResponseCode等细枝末节不在本文讨论

六、代码实现

  1. 新增redis 配置 :pom+配置文件
        
		
			org.springframework.boot
			spring-boot-starter-data-redis
		

#########################本地开发环境#########################
##spring boot 配置
server.port=8004
spring.application.name=share
############################################################
## MySQL配置
spring.datasource.url=jdbc:mysql://192.168.1.12:3306/share?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
############################################################
## Redis配置
spring.redis.host=192.168.1.12
#spring.redis.password=
spring.redis.database=1
spring.redis.port=6379
############################################################

2.自定义注解  @ReSubmitCheck

import java.lang.annotation.*;

/**
 *  在需要保证接口幂等性的Controller的方法上使用此注解
 *  重复提交校验注解
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReSubmitCheck {
    //校验几秒内重复提交
    int seconds()  default 3;
}

 3. 防止重复提交切面处理器 PreventReSummitAspect

import com.city.share.annotation.ReSubmitCheck;
import com.city.share.enums.ResultEnum;
import com.city.share.exception.ShareException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

/**
 *  防重复点击
 */
@Aspect
@Component
@Slf4j
public class PreventReSummitAspect {

    /**
     * redis工具类
     */
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Before("@annotation(reSubmitCheck)")
    public void preventReSubmit(JoinPoint joinPoint, ReSubmitCheck reSubmitCheck) {

        ServletRequestAttributes attributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        //获取用户登录的accesstoken
        HttpServletRequest request = attributes.getRequest();
        String token = request.getParameter("accesstoken");
        if (token == null) {
            throw new ShareException(ResultEnum.ON_LOGIN);
        }
        String lockKey = "ReSubmit:" + token + "_" + request.getServletPath();
        Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey,         reSubmitCheck.seconds(), TimeUnit.SECONDS);

        if (!result) {
            System.out.println("重复请求:"+lockKey);
            throw new ShareException(ResultEnum.RESUBMIT_ERROR);
        }
    }

}

 4.测试controller  HolleContraller

import com.city.share.Dto.Result;
import com.city.share.Utils.ResultUtil;
import com.city.share.annotation.ReSubmitCheck;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.Serializable;

@RestController
public class HolleContraller implements Serializable{

    @GetMapping("/holle")
    @ReSubmitCheck(seconds=10)//这边设置了10秒内不能重复访问
    public Result holleTest(){
        System.out.println("hello spring boot");
        return ResultUtil.success("hello spring boot");
    }
}

OK, 目前为止, 校验代码准备就绪, 接下来测试验证

七、测试验证

访问:127.0.0.1:8004/holle?accesstoken=123456

订单重复提交_springboot + aop + redis + 注解 , 实现接口幂等性校验_第1张图片

 查看redis

订单重复提交_springboot + aop + redis + 注解 , 实现接口幂等性校验_第2张图片

测试接口安全性: 利用jmeter测试工具模拟10个并发请求

订单重复提交_springboot + aop + redis + 注解 , 实现接口幂等性校验_第3张图片

请求结果:因为都是在10秒内,所以只有第一个请求成功

订单重复提交_springboot + aop + redis + 注解 , 实现接口幂等性校验_第4张图片

八、总结

其实思路很简单, 就是每次请求保证唯一性, 从而保证幂等性, 通过spring aop+注解, 就不用每次请求都写重复代码, 其实也可以利用拦截器实现。

如果小伙伴有什么疑问或者建议欢迎提出

参考地址:https://www.cnblogs.com/wangzaiplus/p/10931335.html

源码地址:https://download.csdn.net/download/zppiio/85309475icon-default.png?t=M3K6https://download.csdn.net/download/zppiio/85309475

你可能感兴趣的:(java,redis,spring,boot,java,表单,验证码)