Aop+Redis防止接口重复提交

2022-05-13  本文已影响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. 记录一次分布式锁的学习

上一篇下一篇

猜你喜欢

热点阅读