一、为什么要防止接口重复提交?
对于有些敏感操作接口,比如提交数据接口、付款接口,如果用户操作不当,多次点击提交按钮,接口就会被多次请求,最后可能生成重复数据,导致系统异常,影响用户使用。
二、后端解决方案:
- 自定义注解@AvoidDuplicateSubmit 标记所有Controller中提交的请求
- 通过AOP对所有标记了@AvoidDuplicateSubmit 的方法进行拦截
- 切面类实现拦截思路:
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;
三、代码如下:
- maven
<!-- asp -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
- 创建自定义注解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;
}
- 创建切面类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();
}
}
- 使用注解
/**
* 订单确定导入,数据提交
* @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) {
-
名词理解
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不需要任何动作 -
参考文章
1. 利用自定义注解+aop+redis防止重复提交
2. spring aop的@Before,@Around,@After,@AfterReturn,@AfterThrowing的理解
3. Spring AOP
4. Redis 分布式锁
5. 记录一次分布式锁的学习
网友评论