必须带EX参数,否则锁永不释放;正确写法是rdb.SetNX(ctx, key, value, 30*time.Second),TTL需大于接口最长耗时加安全余量。

redis.SetNX必须带EX参数,否则锁永不释放
不设过期时间的 SetNX 在服务崩溃或 panic 后会永久卡住 key,后续所有合法重试都被拦截,用户看到“重复提交”但实际业务根本没成功。这不是防重,是误杀。
正确写法是明确传入 TTL:rdb.SetNX(ctx, key, value, 30*time.Second)。这个 30 秒不是拍脑袋定的——得大于接口最长耗时(比如支付链路含三方回调可能耗时 25 秒)再加至少 5 秒安全余量。超时太短会导致正常请求被误判;太长则占用 Redis 内存、拖慢 key 回收。
- 别用
EXISTS + SET拆成两步:竞态窗口下两个请求同时判断不存在,然后都写入,幂等失效 - value 别填空字符串,存
trace_id或客户端时间戳,出问题能快速关联日志定位 - key 命名要带业务上下文,比如
"idempotent:POST:/orders:user_123:" + sha256sum(params),避免不同接口或用户 key 冲突
指纹不能只拼Idempotency-Key,得绑定业务上下文
客户端传的 X-Idempotency-Key 天然不可信:可能被恶意复用、前端生成逻辑有 bug、甚至多个订单共用同一个 key。裸用它当 Redis key 就等于把校验大门敞开。
真正可复现、可隔离的指纹,必须融合请求语义:method + path + user_id + 参数哈希。例如创建订单,用 sha256(order_amount + goods_id + coupon_id) 而不是整个 JSON body(太宽影响性能且易因空格/顺序微变导致哈希不一致)。
立即学习“go语言免费学习笔记(深入)”;
- GET 请求禁用 query string 做指纹:CDN 或代理可能缓存并重放,导致误判
- 不要用纯时间戳或随机数:客户端生成无法保证全局唯一,服务端也无法验证其有效性
- 如果业务已有天然唯一标识(如
order_no),优先直接用它,省去哈希计算开销
Redis失败不能 fallback 到放行,必须返回503
Redis 连接超时、集群节点失联、读写权限异常——这些都不是“可以忽略”的临时抖动。一旦幂等存储不可用,继续执行业务逻辑就等于放弃幂等语义,可能引发扣款两次、库存超卖等严重后果。
正确姿势是:捕获 rdb.SetNX 的 err(非 nil),且不看返回的 ok bool,直接返回 503 Service Unavailable。前端收到这个状态,应停止重试并提示“系统繁忙,请稍后操作”,而不是默默再发一遍。
- 别在 error 处理里写
log.Warn("redis down, skip idempotent check")—— 这等于主动放弃防护 - 别试图 fallback 到
sync.Map:单机缓存重启即丢,分布式下完全失效 - 如果 Redis 是强依赖,建议在启动阶段做健康检查,提前暴露问题
数据库唯一索引不是备选,而是兜底铁律
所有中间层控制都可能绕过:Redis 故障、客户端绕过 header、网络重传穿透中间件、甚至运维误删 key。DB 层唯一约束是最后一道不可替代的防线。
订单表必须建联合唯一索引,例如 UNIQUE (user_id, idempotency_key)。注意字段宽度:idempotency_key 建议限制为 VARCHAR(64),太宽会影响写性能和索引大小。
- 插入失败后,不能直接返回错误:必须用
pgx.ErrCodeUniqueViolation或mysql.MySQLError.Number == 1062精准识别冲突 - 识别到冲突后,必须查一次 DB 确认该记录是否真已存在——不能假设“报错=已成功”,因为可能 DB 写入成功但响应丢失
- 查 DB 和插入必须在同一个事务外完成;若放在事务内,DB 成功但 Redis 失败,下次请求会因 key 存在被误拒
SetNX,是让 Redis key 的生命周期、数据库唯一索引的覆盖范围、以及业务状态机(pending/success/failed)三者对齐。漏掉任意一环,线上就会出现“看着拦截了,其实数据还是重复了”的情况。


















