• springboot 防止重复提交


    1、重复提交原因

    客户端的抖动,快速操作,网络通信或者服务器响应慢,造成服务器重复处理。防止重复提交,除了从前端控制,后台也需要控制。因为前端的限制不能解决彻底。接口实现,通常要求幂等性,保证多次重复提交只有一次有效。对于更新操作,达到幂等性很难。

    2 、后端防止重复提交方案

    1、基于token

    访问请求到达服务器,服务器端生成token,分别保存在客户端和服务器。提交请求到达服务器,服务器端校验客户端带来的token与此时保存在服务器的token是否一致,如果一致,就继续操作,删除服务器的token。如果不一致,就不能继续操作,即这个请求是重复请求。

    这种方案,每次提交要发送两次请求。对前端不是特别友好。

    2、基于缓存

    request进来,没有就先存在缓存中,继续操作业务,最后删除缓存或者缓存设置生命周期。如果存在,就直接对request进行验证,就不能继续操作业务。

    6887f65f8e1d3dad757f780efad35794.png

    从该图中可以得知,如果当前提交的请求URL已经存在于缓存中,且当前提交的请求体 跟缓存中该URL对应的请求体一毛一样 且当前请求URL的时间戳跟上次相同请求URL的时间戳 间隔在8s 内,即代表当前请求属于 “重复提交”;如果这其中有一个条件不成立,则意味着当前请求很有可能是第一次请求,或者已经过了8s时间间隔的 第N次请求了,不属于“重复提交”了。

    3、代码实现

    照着这个思路,接下来我们将采用实际的代码进行实战,其中涉及到的技术:Spring Boot2.6 + 自定义注解 + 拦截器 + Redis缓存 (也可以分布式缓存Redisson);

    1、pom 关键依赖如下:

    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.0.M4</version>
    </dependency>
    复制代码

    2.配置文件如下:

    server.port=8888
    
    # Redis数据库索引(默认为0)
    spring.redis.database=0
    # Redis服务器地址
    spring.redis.host=127.0.0.1
    # Redis服务器连接端口
    spring.redis.port=6380
    # Redis服务器连接密码(默认为空)
    spring.redis.password=eco.dameng.com
    # 连接池最大连接数(使用负值表示没有限制)
    spring.redis.pool.max-active=8
    # 连接池最大阻塞等待时间(使用负值表示没有限制)
    spring.redis.pool.max-wait=-1
    # 连接池中的最大空闲连接
    spring.redis.pool.max-idle=8
    # 连接池中的最小空闲连接
    spring.redis.pool.min-idle=0
    # 连接超时时间(毫秒)
    spring.redis.timeout=5000
    复制代码

    3、注解RepeatSubmit 如下:

    @Inherited
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RepeatSubmit {
       
       /**
        * 防重复操作限时标记数值(存储redis限时标记数值)
        */
       String value() default "value" ;
       
       /**
        * 防重复操作过期时间(借助redis实现限时控制)
        */
       long expireSeconds() default 10;
    }
    复制代码

    4、自定义拦截器

    @Slf4j
    @Component
    @Aspect
    public class NoRepeatSubmitAspect  {
       
       
       @Autowired
       private RedisTemplate redisTemplate;
       /**
        * 定义切点
        */
       @Pointcut("@annotation(com.example.learn.annotaion.RepeatSubmit)")
       public void preventDuplication() {}
       
       @Around("preventDuplication()")
       public Object around(ProceedingJoinPoint joinPoint) throws Exception {
          
          /**
           * 获取请求信息
           */
          ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
          
          HttpServletRequest request = attributes.getRequest();
          
          // 获取执行方法
          Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
          
          //获取防重复提交注解
          RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
          
          // 获取token以及方法标记,生成redisKey和redisValue
          String token = request.getHeader(IdempotentConstant.TOKEN);
          
          String url = request.getRequestURI();
          
          /**
           *  通过前缀 + url + token + 函数参数签名 来生成redis上的 key
           *
           */
          String redisKey = IdempotentConstant.PREVENT_DUPLICATION_PREFIX
                .concat(url)
                .concat(token)
                .concat(getMethodSign(method, joinPoint.getArgs()));
          
          // 这个值只是为了标记,不重要
          String redisValue = redisKey.concat(annotation.value()).concat("submit duplication");
          
          if (!redisTemplate.hasKey(redisKey)) {
             // 设置防重复操作限时标记(前置通知)
             redisTemplate.opsForValue()
                   .set(redisKey, redisValue, annotation.expireSeconds(), TimeUnit.SECONDS);
             try {
                //正常执行方法并返回
                //ProceedingJoinPoint类型参数可以决定是否执行目标方法,
                // 且环绕通知必须要有返回值,返回值即为目标方法的返回值
                return joinPoint.proceed();
             } catch (Throwable throwable) {
                //确保方法执行异常实时释放限时标记(异常后置通知)
                redisTemplate.delete(redisKey);
                throw new RuntimeException(throwable);
             }
          } else {
             // 重复提交了抛出异常,如果是在项目中,根据具体情况处理。
             throw new RuntimeException("请勿重复提交");
          }
          
       
       }
       
       /**
        * 生成方法标记:采用数字签名算法SHA1对方法签名字符串加签
        *
        * @param method
        * @param args
        * @return
        */
       private String getMethodSign(Method method, Object... args) {
          StringBuilder sb = new StringBuilder(method.toString());
          for (Object arg : args) {
             sb.append(toString(arg));
          }
          return DigestUtil.sha1Hex(sb.toString());
       }
       
       private String toString(Object arg) {
          if (Objects.isNull(arg)) {
             return "null";
          }
          if (arg instanceof Number) {
             return arg.toString();
          }
          return JSONUtil.toJsonStr(arg);
       }
       
    }
    复制代码

    5、其他类文件

    实体测试类

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Order {
       
       private String orderNo;
       
       private String productName;
       
       private String purchaseName;
    }
    复制代码

    常量类

    public interface IdempotentConstant {
       
       
       String TOKEN = "token";
       
       String PREVENT_DUPLICATION_PREFIX = "PREVENT_DUPLICATION_PREFIX:";
    }
    复制代码

    6、控制器类

    @Slf4j
    @RestController
    @RequestMapping("/web")
    public class IdempotentController {
       
       
       @PostMapping("/sayNoDuplication")
       @RepeatSubmit(expireSeconds = 8)
       public String sayNoDuplication(@RequestParam("requestNum") String requestNum) {
          log.info("sayNoDuplicatin requestNum:{}", requestNum);
          return "sayNoDuplicatin".concat(requestNum);
       }
       
       
       @PostMapping("/addOrder")
       @RepeatSubmit(expireSeconds = 8)
       public String addOrder(@RequestBody Order order) {
          log.info("addOrder requestNum:{}", order);
          return JSONUtil.toJsonStr(order);
       }
    }
    复制代码

    4、测试

    访问 http://localhost:8888/web/sayNoDuplication

    image.png

    第一次访问 image.png

    多次点击

    image.png

    5、总结

    基于JAVA注解+AOP切面快速实现防重复提交功能,该方案实现可以完全胜任非高并发场景下实施应用。但是在高并发场景下仍然有不足之处,存在线程安全问题(可以采用Jemeter复现问题)

    if (!redisTemplate.hasKey(redisKey)) { // 设置防重复操作限时标记(前置通知) redisTemplate.opsForValue() .set(redisKey, redisValue, annotation.expireSeconds(), TimeUnit.SECONDS);

    主要是这个操作不是原子的,在高并发场景会有问题。可以使用 redisson 分布式锁进行解决。

    来源:https://juejin.cn/post/7091860233693167647
  • 相关阅读:
    linux 清空文件内容命令
    优秀的java 社区
    vue强制刷新组件 ----组件重置到初始状态
    function的json对象转换字符串与字符串转换为对象的方法
    js实现深度优先遍历和广度优先遍历
    Egg.js中使用sequelize事务
    JavaScript ES6 数组新方法 学习随笔
    eggjs的参数校验模块egg-validate的使用和进一步定制化升级
    Node.js 服务端图片处理利器
    webp图片实践之路
  • 原文地址:https://www.cnblogs.com/konglxblog/p/16756633.html
Copyright © 2020-2023  润新知