必须用setIfAbsent而非exists+set,因其是原子操作,可杜绝高并发下的竞态条件;超时时间必须设置,避免key永久残留导致误拦截;token应优先从请求头读取,业务异常时需确保key自动过期而非手动清理。

必须用 setIfAbsent,不能用 exists + set
高并发下,exists 判断后再 set 会直接失效——两个请求几乎同时通过 exists 检查,都写入成功,幂等性彻底崩坏。这是典型的竞态条件,不是“概率低”,而是“必现”。setIfAbsent(对应 Redis 的 SETNX)是原子操作:写入和判断合为一步,不可中断。
Spring Data Redis 的封装已足够安全:redisTemplate.opsForValue().setIfAbsent(key, value, timeout) 底层就是 SET key value EX seconds NX,别手写 execute() 调原生命令,除非你明确需要 Lua 脚本控制更复杂的逻辑(比如校验+删除一步完成)。
- 超时时间(
timeout)必须设,否则 key 永久残留,后续所有合法重试都会被拦截 -
value本身无业务意义,填"1"、System.currentTimeMillis()或空字符串都行,只依赖 key 的存在性 - 返回
true表示首次写入成功(可执行业务),false表示已被占用(应拒绝请求)
Token 该从哪里取?Header 是首选
优先从请求头(如 X-Idempotent-Token)读取 token。它和业务参数完全解耦,前端 Axios 拦截器能统一注入,后端也容易在 AOP 切面里提取。GET 请求不会因带 token 导致 URL 被缓存或记录在 Nginx access_log 里,规避了敏感信息泄露风险。
不推荐的方案:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 不要从 URL query 取:浏览器历史、CDN、代理日志全会留下痕迹
- 不要依赖 Cookie:跨域场景下失效,且无法绑定到单次请求
- 若必须放 body(如表单提交),要确认网关不 strip body;JSON 接口则需 DTO 字段名与传入字段严格一致,否则反序列化失败导致校验跳过
@Idempotent 注解 + AOP 的关键陷阱:异常路径下的 key 清理
AOP 拦截本身没问题,但常见错误是把 token 校验逻辑包在 try 块里,或者没处理业务异常后的清理逻辑。一旦业务方法抛出异常,token 还留在 Redis 中,后续合法重试会被误拒——这不是幂等,是“锁死”。
正确做法是:
- 校验通过后立即调用
setIfAbsent写入 Redis - 用
try...finally包裹业务逻辑,但 不在finally里删 key(防止成功后因其他异常误删) - 只在业务执行成功后,显式调用
delete或让 key 自动过期(更推荐后者,避免 delete 失败导致残留)
Redis 版本与安全:别忽略 CVE-2025-62507
如果你还在用低于 Redis 8.2.3 的版本,CVE-2025-62507 这个高危 RCE 漏洞可能让你的幂等校验服务变成攻击入口。该漏洞影响所有启用了 Lua 脚本执行的生产实例,而幂等性方案中恰好常用 Lua 实现原子校验+删除。8.2.3 不仅修复了该漏洞,还稳定了 HyperLogLog 等结构在高频写入下的崩溃问题。
升级不是“建议”,是上线前必须验证的动作。本地开发用 7.x 无所谓,但生产环境 Redis 必须是 8.2.3 或更高。

















