限制接口重复提交
涉及的点: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块上面的代码):会跳转到异常通知的代码逻辑中进行锁的释放(拿到锁就释放,未拿到就忽略)