必须用客户端提供的idempotency-key+业务上下文构造唯一指纹,配合redis原子写入和db唯一索引双保险;纳秒时间戳易重复且脱离业务,body哈希受格式和中间件干扰,无法区分重试与新请求。

必须在服务端用客户端提供的 Idempotency-Key + 业务上下文构造唯一指纹,配合 Redis 原子写入和 DB 唯一索引双保险——前端防重、HTTP 方法语义、时间戳或 body 哈希,全都不靠谱。
为什么不能用 time.Now().UnixNano() 或请求体哈希当幂等 key
纳秒级时间戳在容器或高并发下极易重复,且完全脱离业务意图;body 哈希则受字段顺序、空格、gzip、代理改写等干扰,同一逻辑请求可能生成不同 hash。更糟的是,这些方式无法区分“用户重试”和“新请求”,比如刷新页面后提交相同订单,本该命中幂等,却因 key 变了被当作新操作执行。
- 真正有效的 key 必须绑定业务实体,例如
"idempotent:order_create:" + userID + ":" + idempotencyKey -
idempotencyKey应由客户端生成(推荐uuid.NewString()),通过X-Idempotency-Keyheader 传递,服务端不生成、不修改 - 绝对不要用
Content-MD5或json.Marshal后的哈希——客户端未必支持,中间件可能改写 body
redis.Client.Set() 必须带 NX 和 EX 参数,不能拆成两步
先 GET 再 SET 或先 SETNX 再 EXPIRE 都存在竞态:若中间崩溃或网络失败,key 就永久滞留,后续所有合法请求全被拦截,等于服务不可用。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 正确写法(基于
github.com/redis/go-redis/v9):client.Set(ctx, key, "1", 5*time.Minute).Err()—— 它底层自动拼装SET key value EX 300 NX原子命令 - TTL 时间要大于接口最长可能耗时(含下游依赖超时),例如支付回调链路最长 45s,TTL 至少设为
60 * time.Second - 如果
Set()返回redis.Nil,说明 key 已存在,应直接返回409 Conflict,响应体结构与首次成功一致,仅加"idempotent": true字段
数据库唯一索引不是可选,是必须的兜底
Redis 可能宕机、网络分区、或写入成功但业务 panic 导致未清理,单靠缓存永远无法 100% 保证幂等。DB 层必须用唯一约束做最终防线。
- 例如订单表,除主键外,必须建联合唯一索引:
UNIQUE INDEX idx_user_id_idempotency_key (user_id, idempotency_key) - Go 中捕获冲突错误要精准:
if pgx.ErrCodeUniqueViolation == err.(pgconn.PgError).Code或 MySQL 的errno 1062 - 绝不能把所有 error 都吞掉并返回 success——只有唯一键冲突才算幂等成功,其他错误(如连接超时、校验失败)必须原样暴露
- 索引字段别太宽,
idempotency_key控制在 32 字符内,避免拖慢写性能
最易被忽略的一点:幂等结果缓存必须区分状态。不是简单存个 true,而是存结构体 {status: "success", result: json.RawMessage, timestamp: time.Time},这样第二次请求才能原样返回 body 和 status code,而不是只返回 200 空响应。否则前端无法正确更新 UI 状态。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










