必须用setifabsent而非set,因其是原子操作:在写入key时同步判断是否存在,不可中断;而先exists再set会引发竞态条件,导致高并发下重复写入。

为什么必须用 setIfAbsent 而不是 set?
因为只有 setIfAbsent(对应 Redis 的 SETNX)是原子操作:它在写入 Key 的同时判断是否存在,整个过程不可打断。如果先 exists 再 set,高并发下两个请求可能同时通过 exists 判断,导致重复写入——这就是典型的竞态条件。
Spring Data Redis 提供的 redisTemplate.opsForValue().setIfAbsent(key, value, timeout) 是安全封装,底层调用的就是 SET key value EX seconds NX。别手写 execute() 调用原生命令,除非你明确需要自定义 Lua 脚本逻辑。
- 超时时间必须设,否则 Redis 中残留的 key 会永久阻塞后续合法请求
- value 不必有意义,填
"1"或当前时间戳都行,重点是 key 的存在性 - 返回
true表示首次写入成功(可执行业务),false表示已被占用(应拦截)
Token 是从请求头还是参数里取?怎么选?
优先从请求头(如 X-Idempotent-Token)取。这样能和业务参数解耦,避免 GET 请求因带 token 导致 URL 缓存/日志泄露,也方便前端统一注入(比如 Axios 拦截器自动加 header)。
若必须放 body(如表单提交),需注意:GET 请求无法携带 body,且部分网关或代理会 strip body;JSON 接口则需确保反序列化不失败(例如 token 字段名要和 DTO 字段一致)。
- 不要从 URL query 取 token:会被浏览器历史、CDN、Nginx access_log 记录,有安全风险
- 不要依赖 Cookie 自动携带:跨域场景失效,且无法精确绑定到某次请求
- 前端生成 token(如
UUID.randomUUID().toString())即可,无需服务端签名——只要保证唯一性和短期有效
@Idempotent 注解 + AOP 切面的实际坑点
AOP 拦截本身没问题,但常见错误是把 token 校验逻辑写在 try 块里,或者没处理异常路径下的 key 清理。一旦业务方法抛异常,token 还留在 Redis,导致后续合法重试也被拒。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
正确做法是:校验通过后立即写入 Redis,然后 try...finally 中执行业务,**不在 finally 里删 key**(防止成功后异常导致误删),而是在业务成功后显式删除(或依赖过期)。
- 拦截器中未校验 token 是否为空,直接传给 Redis,触发
NullPointerException - Redis 连接异常时,
setIfAbsent抛RedisConnectionFailureException,应降级为允许请求(记录告警),而非直接 500 - 切面作用范围太宽(比如匹配所有
@PostMapping),把查询类接口也套进去了,徒增 Redis 压力
Key 的设计为什么不能只用 token?
单纯用 "IDEMPOTENT:" + token 在多数场景够用,但遇到「同一用户对同一订单多次支付」这类业务,就容易误判。比如用户第一次支付失败,重试时用了新 token,但旧 token 还没过期,此时两个不同 token 对应同一笔订单,Redis 无法识别语义重复。
更健壮的做法是组合业务标识:例如 "IDEMPOTENT:PAY:" + userId + ":" + orderId,或对关键参数(如金额、商品 ID、时间窗口)做哈希后拼接。这样即使 token 不同,只要业务上下文一致,仍能命中同一个 key。
- 不要把明文敏感参数(如手机号、身份证号)直接拼进 key,避免 Redis RDB/AOF 泄露
- key 长度不宜过长,超过 1KB 会影响 Redis 性能和内存碎片
- 若用参数哈希,注意 JSON 序列化顺序(如用
TreeMap或固定字段顺序),否则相同参数可能生成不同 hash
真正难的是权衡:短过期时间(如 2 分钟)防堆积但容错低;长过期(如 30 分钟)保重试但占内存。线上建议从 5 分钟起步,再根据监控(Redis key 数量、setIfAbsent 失败率)动态调整。










