SpringAop和Redis实现分布式锁限制接口重复提交

目录

  • 限制接口重复提交
    • SpringAop+Redis实现分布式锁
      • 自定义注解
      • aop切面
      • 涉及枚举
      • 归纳

限制接口重复提交

涉及的点:SpringAop切面、Redis、自定义注解

SpringAop+Redis实现分布式锁

自定义注解

//作用目标在方法上
@Target(ElementType.METHOD)
//表示该注解可以在运行时通过反射进行访问
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LimitSubmit {
    //标识:此场景就是用来限制同一用户新增操作多次点击接口重复调用的问题(暂时不用)
    String key() default "";

    //锁的过期时间,默认10s作为接口的最大执行时间,超过时间锁过期释放
    int aliveTime() default 10;

    //自定义注解错误提示信息
    String errorMsg();
}

aop切面

/**
 * @Author matengfei
 * @Date 2023/11/7 21:09
 * @PackageName:org.jeecg.modules.lock.aop
 * @ClassName: LimitSubmitAspect
 * @Description: 对注解进行切面,切注解,获取被该注解修饰的方法
 * @Version 1.0
 */
//切面:切入点+通知
@Aspect
@Slf4j
@Component
public class LimitSubmitAspect {

    @Autowired
    private RedisUtil redisUtil;

    /**
     * @Description:被该注解标注的方法上将会被执行切面逻辑
     * Signature:获取连接点的方法签名,方法
     * @param joinPoint
     * @return void
     */
    @Around("@annotation(org.jeecg.modules.lock.annotation.LimitSubmit)")
    public void interfaceLimitSubmit(ProceedingJoinPoint joinPoint){
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        //获取自定义注解信息
        LimitSubmit annotation = method.getAnnotation(LimitSubmit.class);
        //锁的过期时间
        int aliveTime = annotation.aliveTime();
        //锁的key
        String prefix = annotation.key();
        LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
        String key = prefix + loginUser.getId();
        //加锁之前判断是不是之前对这个key加过锁
        Object result = redisUtil.get(key);
        if (null!=result){
            throw new LimitSubmitException(annotation.errorMsg());
        }
        //进行加锁操作
        redisUtil.set(key,new Timestamp(System.currentTimeMillis()),aliveTime);
        //开始继续执行连接点的方法!!!
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            log.info("连接点方法执行异常!");
        } finally {
            //进行释放锁操作
            releaseLock(key);
        }
    }

    /**
     * @Description:通知方法执行过程中出现异常进行释放锁操作
     * 原因:try之前的代码如果抛出异常,finally代码块释放锁的操作不会执行
     * @param joinPoint
     * @return void
     */
    @AfterThrowing(value = "@annotation(org.jeecg.modules.lock.annotation.LimitSubmit)",throwing = "ex")
    public void forTargetMethodsException(JoinPoint joinPoint, Throwable ex){
        //连接点抛出这个异常表示接口被同一用户重复点击,表示用户第一次调用接口的业务逻辑还没有执行完成
        //但是切面定义的通知里面可能会抛出人为不知道的异常,可能是在加锁之后try语句块的上面,此时这里就不会出发finally中的解锁操作

        //获取key的前缀
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        LimitSubmit annotation = signature.getMethod().getAnnotation(LimitSubmit.class);
        String prefix = annotation.key();
        LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
        String key = prefix + loginUser.getId();
        //非 接口重复提交 异常 进行解锁操作
        if (!(ex instanceof LimitSubmitException)){
            //解锁操作
            releaseLock(key);
        }
    }

    /**
     * @Description:释放锁操作
     * @param key
     * @return void
     */
    public void releaseLock(String key){
        redisUtil.del(key);
        log.info("锁:{},释放操作完成!",key);
    }
}

涉及枚举

public interface DistributeEnum {
    String LIMIT_SUBMIT_LOCK = "limit_submit:";
}
public interface ErrorMsgEnum {
    String STOP_REPEAT_SUBMIT = "请勿重复提交!";
}

归纳

加锁、解锁过程未出现异常

  • 业务逻辑执行时间>锁的过期时间:存在一种极端的情况,锁已经过期了,第一次请求调用的新增接口还没有执行完,那么用户不知道第一次是否成功执行了,再次点击这次请求会成功获取到锁,会再执行一次新增请求,两次请求完成之后数据库会新增两条一模一样的数据
  • 业务逻辑执行时间<锁的过期时间:正常逻辑,第一次请求执行完成才会释放锁,并将返回结果响应给用户,前端拿到响应结果会进行页面交互,这时用户就不能拿着原来的数据重复调用新增接口了

加锁、解锁过程出现异常

  • 用户重复调用接口(加锁的主要目的就是防止这个的):用户的第一次请求会成功拿到锁执行业务逻辑,之后用户点击的重复请求不会拿到锁,会进行定义的切面通知中抛出指定的异常,给用户进行信息提示
  • 用户重复调用接口(出现未知异常):连接点方法执行出现未知异常:会被catch块捕获执行finally块内的释放锁的代码逻辑切面中的通知出现异常(连接点方法执行之前,也就是try块上面的代码):会跳转到异常通知的代码逻辑中进行锁的释放(拿到锁就释放,未拿到就忽略)

你可能感兴趣的:(SpringBoot,分布式锁,redis,SpringAop)