处理重复请求的心得

tech2025-12-24  2

在很多业务场景我们会有重复请求的校验处理 常见解决办法基本都是用数据库的唯一索引来处理,但其实这么做对于一个成熟的项目会有很多局限性,例如: 1:对数据表的局限比较大,例如表里可能存在多个字段才能保证唯一; 2:很多时候用组合唯一索引才能保证行唯一的情况; 3:业务随时发生变化,后边还要在组合索引中再加新增列才能保证行唯一;对于大数据量在线ddl很可能会影响用户使用 4:很多业务都需要重复校验,多次校验实现会比较耗时,不通用

基于spring boot、redis template我写了一个防止重复请求的控制,直接贴代码。 自定义注解、枚举类

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NonRepeat { /** * 锁定时间 seconds * @return */ int lockTime() default 10; /** * lockKey策略 * @see NonRepeatEnum */ NonRepeatEnum lockKeyPolicy() default NonRepeatEnum.TOKEN; /** * 前缀 repeat:{className}:{methodName}: * 默认根据 token * 支持 spEL 取值非string类型会序列化为json,JSON.toJsonString * @return */ String lockKey() default ""; /** * 是否在执行结束后释放锁 * @return */ boolean releaseAfterReturn() default true; } /** * 重复请求枚举 * TOKEN 防止当前登录用户重复请求,防止重复处理 * FINAL 指定key 当前controller只能有一个人访问(使用场景不多) * SP_EL 根据入参 根据controller参数校验重复处理 * @author gary.wang * @since 2019/11/16 16:50 **/ public enum NonRepeatEnum { TOKEN, FINAL, SP_EL; }

拦截器 处理

@Aspect @Component public class NonRepeatAspect { @Autowired private RedisTemplate<String, String> redisTemplate; private ExpressionParser parser = new SpelExpressionParser(); @Around(value = "@annotation(nonRepeat)") public Object before(ProceedingJoinPoint pjp, NonRepeat nonRepeat) throws Throwable { String lockKey = ""; switch (nonRepeat.lockKeyPolicy()) { case FINAL: lockKey = nonRepeat.lockKey(); break; case SP_EL: if (StringUtils.isEmpty(nonRepeat.lockKey())) { throw new BizsException(ResultEnum.VALIDATE_PARAM_ERROR); } Expression exp = parser.parseExpression(nonRepeat.lockKey()); StandardEvaluationContext ctx = new StandardEvaluationContext(); ctx.setRootObject(pjp.getArgs()); Object value = exp.getValue(ctx); if (value instanceof String) { lockKey = String.valueOf(value); } else { lockKey = JSON.toJSONString(value); } break; case TOKEN: //自定义UserLocal,从threadLocal中获取用户的token,根据自己项目取token的方法自己改改 lockKey = UserLocal.USER.getToken(); break; default: throw new NullPointerException("not find this policy"); } Object result; String className = pjp.getTarget().getClass().getName(); String methodName = pjp.getSignature().getName(); lockKey = String.format(RedisConstants.REDIS_REPEAT_LOCK, className, methodName).concat(lockKey); //获取锁,这行代码自行封装,可以直接用jedis.setnx RedisLock redisLock = new RedisLock(redisTemplate, lockKey, Expiration.seconds(nonRepeat.lockTime())); if (redisLock.getLock()) { result = pjp.proceed(); } else { throw new BizsException(ResultEnum.NON_REPEAT_ERROR); } if (nonRepeat.releaseAfterReturn()) { redisLock.close(); } return result; }

RedisLock 可以在其他场景用,释放锁可以使用try with resource特性,不需要释放锁的场景可以不使用

@Data //lombok @AllArgsConstructor public class RedisLock implements Closeable { private RedisTemplate<String, String> redisTemplate; private String lockKey; private String lockValue; /** * Expiration.seconds(expiration) */ private Expiration expiration; /** * 不传递值默认为当前时间 yyyy-MM-dd HH:mm:ss.SSS * * @param redisTemplate * @param lockKey * @param expiration */ public RedisLock(RedisTemplate<String, String> redisTemplate, String lockKey, Expiration expiration) { this.redisTemplate = redisTemplate; this.lockKey = lockKey; this.lockValue = DateTime.now().toString(DateUtil.COMMON_PATTERN_SS); this.expiration = expiration; } /** * 获取锁 */ public Boolean getLock() { if (StringUtils.isEmpty(this.lockKey) || StringUtils.isEmpty(this.lockValue)) { return false; } return redisTemplate.execute((RedisCallback<Boolean>) connection -> { RedisSerializer<String> serializer = redisTemplate.getStringSerializer(); //保证原子操作SET_IF_ABSENT boolean locked=connection.set(serializer.serialize(this.lockKey), serializer.serialize(this.lockValue), this.expiration, RedisStringCommands.SetOption.SET_IF_ABSENT); log.info("getLock:{}:{}:{}:{}:{}",locked,this.lockKey,this.lockValue,this.expiration.getTimeUnit(),this.expiration.getExpirationTime()); return locked; }); } //忽略该方法,本文中没有使用到 try with resource场景 @Override public void close() { String releaseTime = DateTime.now().toString(DateUtil.COMMON_PATTERN_SS); Boolean delFlag = redisTemplate.delete(this.lockKey); log.info("lock-close:{}:{}:releaseTime:{}:delFlag:{}", this.lockKey, this.lockValue, releaseTime, delFlag); } }

作用于controller,service

1:默认通过token校验重复请求 //lockKey=repeat:com.xxx.TestController:testRepeat:用户token @NonRepeat(lockTime=60) 2:一般用于全局的重复请求校验 //lockKey=repeat:com.xxx.TestController:testRepeat:send_mail @NonRepeat(lockTime=60,lockKeyPolicy = NonRepeatEnum.FINAL,lockKey="send_mail") 3:通过传入参数 支持sp_el写法 //lockKey=repeat:com.xxx.TestController:testRepeat:第二个参数的name属性(这里需要注意,不同的web容器获取参数的下标可能不一样) @NonRepeat(lockTime=60,lockKeyPolicy = NonRepeatEnum.SP_EL,lockKey = "[1].name") public ResponseResult<List<TestVo>> testRepeat(@RequestParam TestQO qo, @RequestBody TestQO q1) throws BizsException { } //lockKey=repeat:com.xxx.TestController:testRepeat:第一个参数 @NonRepeat(lockTime=60,lockKeyPolicy = NonRepeatEnum.SP_EL,lockKey = "[0]") public ResponseResult<List<TestVo>> testRepeat(@RequestParam String phoneNumber, @RequestParam String username) throws BizsException { }
最新回复(0)