美文网首页
Aop+Redis防止接口重复提交

Aop+Redis防止接口重复提交

作者: 一只浩子 | 来源:发表于2022-05-13 17:00 被阅读0次
    一、为什么要防止接口重复提交?

    对于有些敏感操作接口,比如提交数据接口、付款接口,如果用户操作不当,多次点击提交按钮,接口就会被多次请求,最后可能生成重复数据,导致系统异常,影响用户使用。

    二、后端解决方案:
    1. 自定义注解@AvoidDuplicateSubmit 标记所有Controller中提交的请求
    2. 通过AOP对所有标记了@AvoidDuplicateSubmit 的方法进行拦截
    3. 切面类实现拦截思路:
      3.1 同一ip地址的用户在xx秒内同一方法和参数只能提交成功一次;
      3.2 生成本次提交的唯一key, lockKey = ip_ hashCode
      前缀 = ip ,后缀 = 用户ID+获取类名+方法名+参数的hashCode;
      3.3 利用redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS); 这个方法的特性来保证唯一执行一次方法。
      解释setIfAbsent:缓存放入并设置过期时间,如果不存在就添加,返回true,如果存在,不会做任何操作,返回false;
    三、代码如下:
    1. maven
    <!-- asp  -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
    
    1. 创建自定义注解AvoidDuplicateSubmit
    /**
     * 防止重复提交注解
     * @DateTime: 2022/5/7 下午2:17
     * @Author: zenghao
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AvoidDuplicateSubmit {
    
        /**
         * @return long
         * @Description 指定时间内不可重复提交,单位秒
         **/
        long timeout() default 2;
    
    }
    
    
    1. 创建切面类AvoidDuplicateSubmitAspect
    /**
     * 防止重复提交注解切面
     * @DateTime: 2022/5/7 下午2:16
     * @Author: zenghao
     */
    @Component
    @Aspect
    public class AvoidDuplicateSubmitAspect {
    
        private static final Logger log = LoggerFactory.getLogger(AvoidDuplicateSubmitAspect.class);
    
        @Autowired
        private RedisUtil redisUtil;
    
        /**
         * 定义切入点
         */
        @Pointcut("@annotation(com.yuanben.scms.base.annotation.duplicate.AvoidDuplicateSubmit)")
        public void noRepeat() {}
    
    
        @Around("noRepeat()")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    
            // 获取request对象
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
            Assert.notNull(request, "request not null");
            // 获取用户信息
            GlobalSession globalSession = ControllerUtil.getGlobalSession(request);
            String userId = globalSession.getUserId();
            // 获取ip
            String ip = ControllerUtil.getClientIp(request);
    
            // 获取方法签名
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            // 获取类名、方法名、参数
            String className = method.getDeclaringClass().getName();
            String methodName = method.getName();
            Map<String, String[]> parameMap = request.getParameterMap();
            StringBuilder parameStr = new StringBuilder("");
            // js 会传这个参数variableParameter(时间戳)
            for (Map.Entry<String, String[]> entry : parameMap.entrySet()) {
                if (!"variableParameter".equals(entry.getKey())) {
                    parameStr.append(Arrays.toString(entry.getValue()));
                }
            }
            // 拼接key
            String lockKey_last = String.format("%s#%s#%s#%s", userId, className, methodName, parameStr);
            int hashCode = Math.abs(lockKey_last.hashCode());
            // 拼接redisKey,如:127.0.0.1_1898984393
            String lockKey = StaticValue.COMMON_DUPLICATE_SUBMIT + ":" + String.format("%s_%d", ip, hashCode);
            log.info("lockKey = " + lockKey + "   lockKey_last =" + lockKey_last);
            // 获取注解的过期时间
            AvoidDuplicateSubmit avoidDuplicateSubmit = method.getAnnotation(AvoidDuplicateSubmit.class);
            long timeout = avoidDuplicateSubmit.timeout();
            if (timeout <= 0) {
                timeout = 2;
            }
            // 从redis获取数据
            Object redisValue = redisUtil.get(lockKey);
            // 判断是否存在
            if (!Objects.isNull(redisValue)) {
                throw new JsonException("请勿重复提交");
            }
            // 第一次提交,插入redis
            boolean resultBoolean = redisUtil.setIfAbsent(lockKey, Tools.getUUID(), timeout);
            // 如果失败,说明存在
            log.info("resultBoolean =" + String.valueOf(resultBoolean));
            if (!resultBoolean) {
                throw new JsonException("请勿重复提交");
            }
            // 继续执行方法
            return joinPoint.proceed();
        }
    }
    
    1. 使用注解
     /**
        * 订单确定导入,数据提交
        * @Params: [times, orderType, remark, redisKey]
        * @DateTime: 2021/7/30 下午6:34
        * @Author: zenghao
        */
        @AvoidDuplicateSubmit(timeout = 5)
        @ResponseBody
        @RequestMapping(value = "/submit")
        public AjaxResponse<Object> submit(String times, String rcvAddress, String orderType, String remark, String redisKey) {
    
    1. 名词理解
      5.1 @Around 环绕通知(Around advice) :包围一个连接点的通知,类似Web中Servlet规范中的Filter的doFilter方法。可以在方法的调用前后完成自定义的行为,也可以选择不执行。这时aop的最重要的,最常用的注解。用这个注解的方法入参传的是ProceedingJionPoint pjp,可以决定当前线程能否进入核心方法中——通过调用pjp.proceed();
      5.2 setIfAbsent底层实现:
      Redis setnx 命令,SET if Not exists,即在键值对不存在的时候才能设值成功。
      作用:将key的值设置成value,当且仅当key不存在,若给定的key已经存在,则setnx不需要任何动作

    2. 参考文章

    1. 利用自定义注解+aop+redis防止重复提交
    2. spring aop的@Before,@Around,@After,@AfterReturn,@AfterThrowing的理解
    3. Spring AOP
    4. Redis 分布式锁
    5. 记录一次分布式锁的学习

    相关文章

      网友评论

          本文标题:Aop+Redis防止接口重复提交

          本文链接:https://www.haomeiwen.com/subject/uhghurtx.html