自定义注解解决重复提交问题
🔒

自定义注解解决重复提交问题

Created
Jun 19, 2024 09:13 AM
Tags
有这样一个接口:
@RestController @RequestMapping("/api/v1/users") @RequiredArgsConstructor public class SysUserController { private final SysUserService userService; @PostMapping public Result saveUser( @RequestBody @Valid UserForm userForm ) { boolean result = userService.saveUser(userForm); return Result.judge(result); } }
如果由于网络问题导致用户多次点击提交按钮,就会多次请求我们的接口,多次到数据库里判断是否有用户的存在,造成压力。其他类似的接口也是如此。
 
短时间内的多次重复请求本质上对应短时间内多个线程重复的申请资源,因此,只需要让这段时间内的请求在真正到达接口前,让它们去争夺锁:第一个请求将得到互斥锁,短时间内其他重复的请求得不到锁时即刻返回,不再继续到达接口,即可解决重复提交的问题。
 

锁的设计

这里使用 Redis 锁,利用 SETNX,我们让请求所在线程往 Redis 上存储一个带过期时间的 Key(value 无所谓),成功返回 TRUE,如果存在则返回 FALSE。利用返回结果(即是否拿到锁),再决定是否放行到接口。
 
这里,第一个成功存入 Key 的请求将到达接口方法,后续重复的请求在过期时间内将无法存入 Key 而拿到 FALSE 结果,直接抛出错误返回。
 

Key的设计

我们的 Key 将标识的信息有:
  • 前缀标识,声明这是一个用于防重复请求的 Key
  • 哪个用户以哪种请求方式请求哪个接口(uri)
这样就能唯一标识某用户的哪个接口资源被暂时锁了。

利用注解

此外,为了如果能通用在其他接口上,我们采用注解+切片的方式实现申请锁的过程,也方便我们统一处理。本文将用自定义的 @PreventDuplicateSubmit 注解解决重复提交问题。
 

创建注解

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface PreventDuplicateSubmit { /** * 防重提交锁过期时间(秒) * <p> * 默认5秒内不允许重复提交 */ int expire() default 5; }
  • @Target(ElementType.METHOD) :定义注解使用范围,这里用在方法上。
  • @Retention(RetentionPolicy.RUNTIME) :在运行时使用,所以应该保留在运行时代码上。
  • @Documented:指示注解应该被 javadoc 工具或类似工具文档化,方便生成文档时包含注解信息。
  • @Inherited:允许注解在继承层次结构中自动传递,子类会继承父类的注解。
 

创建注解切片

@Aspect @Component @Slf4j @RequiredArgsConstructor public class DuplicateSubmitAspect { private final RedissonClient redissonClient; private static final String RESUBMIT_LOCK_PREFIX = "LOCK:RESUBMIT:"; /** * 防重复提交切点 */ @Pointcut("@annotation(preventDuplicateSubmit)") public void preventDuplicateSubmitPointCut(PreventDuplicateSubmit preventDuplicateSubmit) { log.info("定义防重复提交切点"); } @Around("preventDuplicateSubmitPointCut(preventDuplicateSubmit)") public Object doAround(ProceedingJoinPoint pjp, PreventDuplicateSubmit preventDuplicateSubmit) throws Throwable { String resubmitLockKey = generateResubmitLockKey(); if (resubmitLockKey != null) { int expire = preventDuplicateSubmit.expire(); // 防重提交锁过期时间 RLock lock = redissonClient.getLock(resubmitLockKey); boolean lockResult = lock.tryLock(0, expire, TimeUnit.SECONDS); // 获取锁失败,直接返回 false if (!lockResult) { throw new BusinessException(ResultCode.REPEAT_SUBMIT_ERROR); // 抛出重复提交提示信息 } } return pjp.proceed(); } /** * 获取重复提交锁的 key */ private String generateResubmitLockKey() { String resubmitLockKey = null; HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String token = request.getHeader(HttpHeaders.AUTHORIZATION); if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) { token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length()); // 从 JWT Token 中获取 jti String jti = (String) JWTUtil.parseToken(token).getPayload(RegisteredPayload.JWT_ID); resubmitLockKey = RESUBMIT_LOCK_PREFIX + jti + ":" + request.getMethod() + "-" + request.getRequestURI(); } return resubmitLockKey; } }