@Idempotent注解+AOP+Redis原子操作可拦截99%重复请求;关键在用setIfAbsent或Lua脚本保障校验删除原子性,避免exists+delete伪原子导致并发漏判,且key须含userId、接口路径及业务参数以保证粒度精准。

直接用 @Idempotent 注解 + AOP 切面 + Redis 原子操作,就能在不改业务代码的前提下拦截 99% 的重复请求。关键不是“能不能做”,而是“怎么避免校验失效”和“什么时候会漏判”。
为什么不能只用 redis.exists() + redis.delete()
这是新手最常写的伪原子逻辑,但并发下必然出错:两个线程同时 exists 返回 true,都进到业务执行分支,Token 被删一次、剩一次,第二次请求照样通过。
- 必须用原子命令:
redis.setIfAbsent(key, value, timeout, TimeUnit.SECONDS)或 Lua 脚本EVAL "if redis.call('exists', KEYS[1]) == 0 then redis.call('setex', KEYS[1], ARGV[1], ARGV[2]) return 1 else return 0 end" 1 token 60 1 -
setIfAbsent在 Spring Boot 3.2+ 的RedisTemplate中默认是原子的;老版本需确认是否启用 Lettuce(推荐)而非 Jedis - 如果用
StringRedisTemplate,注意 key 编码——中文或特殊字符要URLEncoder.encode(token, StandardCharsets.UTF_8),否则exists可能查不到
@Idempotent 注解该提取哪些参数
注解本身只是标记,真正决定幂等粒度的是 key 构建逻辑。别只拼个固定前缀,否则所有请求共用一个 token,接口直接变串行。
- 基础组合:用户标识(如
userId) + 接口路径(request.getRequestURL()) + 关键业务参数(如orderId或payAmount的 MD5) - 慎用完整请求体:大 JSON 会导致 key 过长(Redis key 最好
- 推荐方案:
"idempotent:" + userId + ":" + md5(path + "-" + orderId),md5 确保长度可控,也防参数顺序干扰 - 过期时间别硬写 60 秒:支付类接口建议 15 分钟(覆盖银行回调窗口),表单类 5–30 秒足够
AOP 切面里必须处理的三个异常分支
很多实现只写了“存在就放行、不存在就拦截”,但真实环境远比这复杂。
- Redis 连接超时或响应为空:不能让业务直接失败,应记录 warn 日志并放行(降级策略),否则 Redis 故障=全站不可用
- Token 校验通过但业务方法抛异常:必须确保 Token 已删除(或已设为已使用状态),否则用户重试仍被拒——建议在
@AfterReturning删除,在@AfterThrowing补删 - 前端传空/非法 token:统一返回
400 Bad Request并带明确提示(如"missing_idempotent_token"),别混同于“重复提交”的409 Conflict
真正难的不是生成和校验 token,而是当 Redis 集群某节点失联、Lua 脚本执行超时、或前端把 token 存在 localStorage 后又被另一个 tab 覆盖时,你得知道哪条日志能快速定位是 key 构建错了,还是 setIfAbsent 返回 false 却没打日志——这些细节,往往决定线上故障是 5 分钟恢复,还是通宵回滚。











