最稳方案是Redis SetNX(带NX+EX)前置拦截+数据库唯一索引兜底+三态状态机保障响应一致性;漏任一层易致双扣款或订单裂变。

复杂业务场景下,Go 服务的幂等接口不能靠单层校验撑住,必须分层设防:Redis SetNX 做前置拦截、DB 唯一索引做兜底、状态机控制响应一致性——漏掉任何一层,都可能在高并发或故障时出现双扣款、订单裂变。
为什么 SetNX 必须带 NX 和 EX 且不能拆开
常见翻车是先 EXISTS 再 SET,或 SetNX 成功后单独调 EXPIRE。前者有竞态窗口(两个请求同时查到不存在,都写入),后者一旦 EXPIRE 失败,key 就永久卡住,后续所有重试全被误拒。
正确姿势只有一条命令:rdb.SetNX(ctx, key, traceID, 300*time.Second)。go-redis/v9 底层自动拼 NX EX 参数,不依赖客户端手动组装。TTL 时间必须 ≥ 接口最长耗时 + 安全余量(如支付回调最长 45s,TTL 至少设 60s)。
-
key不能裸用X-Idempotency-Key,得拼业务上下文,例如"idempotent:pay:1001:pay_abc123" -
value别存空字符串,填traceID或原始idempotencyKey,方便日志对齐 - 返回值
ok == false是正常拦截,不是错误;err != nil才是 Redis 连接异常,必须处理
数据库唯一索引为什么只是兜底,不是主防线
唯一索引(如 UNIQUE (user_id, idempotency_key))只能在 INSERT 时捕获冲突,但无法阻止两个请求同时通过 Redis 校验、一起走到 DB 写入那步——这时至少一个会报 mysql.ErrDupEntry 或 pgx.ErrCodeUniqueViolation,而业务逻辑可能已部分执行(比如发了 MQ、调了下游),状态已不一致。
立即学习“go语言免费学习笔记(深入)”;
所以唯一索引必须配合“失败后主动查库确认”逻辑:捕获唯一冲突后,再 SELECT 一次,确认记录是否真存在、状态是否已完成,再决定返回 200 + 原始结果,还是 400 + 业务错误。
- 索引字段必须覆盖业务关键维度,比如
(user_id, client_order_id),不能只建(idempotency_key) - 别在事务中间做幂等校验,必须放在最外层,否则 DB 成功但 Redis 失败,下次请求会被误拒
- INSERT 失败后不能直接
return,要查 DB 确认真实状态,避免前端因收到 500/409 而反复重试
幂等响应缓存必须分状态,不能只存 success
很多实现只在 Redis 里存个空 key 表示“已处理”,但这样无法区分“已成功”和“正在执行中”。当第二个相同请求进来,你没法告诉它“稍等,前一个还在跑”,只能粗暴返回 409,用户体验差,还可能掩盖真实问题。
推荐用结构体缓存完整状态:{status: "success", result: json.RawMessage, timestamp: time.Time} 或 {status: "processing"}。这样既能拦截重复提交,也能支持“正在处理中”的友好提示,甚至可选地返回原始成功响应。
- 不要用
sync.Map存幂等结果——它不跨进程、无自动过期,只适合单机低 QPS 场景 - Redis 中缓存的 value 建议含
status字段,handler 中根据 status 分支处理 - 如果业务允许,可将成功响应体序列化后存进 Redis,避免重复计算和下游调用
中间件里怎么安全透传幂等标识
在 Gin/Echo 或原生 net/http 中间件里,千万别用闭包或局部变量存 idempotent_key——并发请求会互相污染。也别在 handler 里重新解析 header,容易格式不一致或遗漏校验。
正确做法是:中间件从 c.Request.Header.Get("X-Idempotency-Key") 取值 → 正则校验格式(如 ^[a-zA-Z0-9_-]{12,64}$)→ 拒绝空值 → 构造 key → 通过 context.WithValue(ctx, "idempotent_key", key) 注入 → handler 中统一用 ctx.Value("idempotent_key") 拿。
- header 为空必须拒绝,绝不 fallback 到服务端生成 ID
- key 构造逻辑必须和 Redis 实际使用的完全一致,包括大小写、分隔符、顺序
- Redis 故障时不能静默放行,必须返回
503 Service Unavailable,由上游重试或降级策略处理
真正难的不是写对一行 SetNX,而是让 Redis、DB、状态机、上下文透传四层逻辑在各种异常路径下仍保持语义一致——比如 Redis 写成功但业务 panic,或 DB 提交成功但网络中断导致响应丢失。这些边界 case 才是线上幂等失效的根源。


















