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

限制接口重复提交

涉及的点: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切面

package org.jeecg.common.aop;

import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.security.SecurityUtil;
import org.apache.shiro.SecurityUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.jeecg.common.annotation.LimitSubmit;
import org.jeecg.common.exception.LimitSubmitException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @Author matengfei
 * @Date 2023/12/26 17:59
 * @PackageName:org.jeecg.common.aop
 * @ClassName: LimitSubmitAspect
 * @Description: LimitSubmit自定义注解切面
 * @Version 1.0
 */
@Aspect
@Slf4j
@Component
public class LimitSubmitAspect {
    @Autowired
    private RedisUtil redisUtil;


    // 定义切入点
    @Pointcut("@annotation(org.jeecg.common.annotation.LimitSubmit)")
    public void point(){

    }


    //定义环绕通知,加锁-执行连接点方法-解锁
    @Around(value = "point()")
    public Object executeConnectPoint(ProceedingJoinPoint joinPoint){
        //获取当前登录用户
        LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
        MethodSignature joinPointSignature = (MethodSignature) joinPoint.getSignature();
        LimitSubmit annotation = joinPointSignature.getMethod().getAnnotation(LimitSubmit.class);
        String key = annotation.key();
        String errorMsg = annotation.errorMsg();
        long keepTime = annotation.keepTime();
        if ("".equals(key)){
            key = user.getId();
        }
        //加锁操作
        if (!redisUtil.setIfAbsent(key,String.valueOf(System.currentTimeMillis()),keepTime)){
            log.error("加锁失败!");
            throw new LimitSubmitException(errorMsg);
        }
        //执行连接点方法
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            log.error("连接点方法执行异常!");
        } finally {
            //释放锁操作
            //这里不进行释放锁的操作(新增完数据从新增页面跳转到列表页还是会出现重复数据,就行因为连接点方法执行完成之后我收到解锁了)
            //releaseLock(key);
            return result;
        }

    }

    @AfterThrowing(pointcut = "point()",throwing = "throwable")
    public void beforeTryFindException(JoinPoint joinPoint,Throwable throwable){
        //上面的环绕通知特殊情况下出现异常可能不会触发finally块里面的释放锁操作
        MethodSignature joinPointSignature = (MethodSignature) joinPoint.getSignature();
        LimitSubmit annotation = joinPointSignature.getMethod().getAnnotation(LimitSubmit.class);
        String key = annotation.key();
        if (!(throwable 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块上面的代码):会跳转到异常通知的代码逻辑中进行锁的释放(拿到锁就释放,未拿到就忽略)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
好的,这是一个非常常见的问题。在使用Java Spring AOP编写Redis分布式锁时,您需要使用RedisTemplate类来连接Redis服务器。以下是一个简单的示例代码: ```java @Aspect @Component public class RedisLockAspect { private static final Logger logger = LoggerFactory.getLogger(RedisLockAspect.class); @Autowired private RedisTemplate<String, String> redisTemplate; @Around("@annotation(redisLock)") public Object redisLock(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable { String key = redisLock.key(); String value = UUID.randomUUID().toString(); boolean locked = redisTemplate.opsForValue().setIfAbsent(key, value, redisLock.expire(), TimeUnit.SECONDS); if (!locked) { logger.warn("Failed to acquire Redis lock for key {}", key); throw new RuntimeException("Failed to acquire Redis lock for key " + key); } try { return joinPoint.proceed(); } finally { if (value.equals(redisTemplate.opsForValue().get(key))) { redisTemplate.delete(key); } } } } ``` 这个示例代码使用了Spring AOP的@Aspect注解来定义一个切面,然后使用@Around注解来定义一个环绕通知。在环绕通知中,我们首先获取Redis锁的key和value,然后使用RedisTemplate的setIfAbsent方法来尝试获取锁。如果获取锁失败,则抛出一个RuntimeException。如果获取锁成功,则执行目标方法,并在最后释放锁。 请注意,这只是一个简单的示例代码,实际上您需要更多的代码来处理异常情况、处理锁的超时等问题。但是这个示例代码应该可以帮助您开始编写Redis分布式锁

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凉水不好喝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值