go接口幂等性校验须用redis.setnx原子占位(带nx+ex)、拼业务上下文key、存trace_id为value、故障返回503、db唯一索引兜底、context透传标识、结构体缓存结果、三态状态机对齐生命周期。

Go 语言中做接口幂等性校验,不能只靠客户端传个 X-Idempotency-Key 就判断是否放行——那只是第一步,漏掉重试、重启、分布式节点状态不一致等场景时,重复请求照样会穿透进来。
用 redis.SetNX 做原子占位,必须带 NX 和 EX
最常用也最可靠的手段是 Redis 的原子写入:用 SetNX 判断 key 是否已存在,同时设过期时间。拆成 EXISTS + SET 两步就是竞态漏洞。
-
SetNX(ctx, key, value, ttl)是 Go-Redis v9+ 推荐方式;手写命令要确保参数顺序正确:SET key value EX seconds NX -
ttl必须大于接口最长耗时 + 网络抖动余量(比如超时 30s,至少设 45s),否则 key 刚写入就过期,二次请求直接绕过 -
key别裸用客户端传的idempotency-key,建议拼成"idempotent:" + method + ":" + path + ":" + userID + ":" + idempotencyKey,防不同接口冲突 -
value别填空字符串,存trace_id或客户端时间戳,出问题能快速对日志 - Redis 连接失败时,不能 fallback 到“放行”,得返回
503 Service Unavailable,否则幂等语义崩塌
用 sync.Map 做单机幂等,但得自己管过期和结构体缓存
小流量、单机部署、QPS 几百以内,sync.Map 比 Redis 更快且无依赖,但它不支持自动过期,也不跨进程。
- 写入前必须先
Load,命中就直接返回原始响应;没命中才执行业务逻辑,完成后必须Store结构体,不能只存bool - 缓存值推荐结构体:
{status: "success", result: json.RawMessage, timestamp: time.Time},否则“已存在”时没法安全返回上次结果 - key 构造别混
context或临时变量,否则并发下污染 map;建议固定格式:"idempotent:" + method + ":" + path + ":" + clientID + ":" + idempotencyKey - 必须配定时清理:比如每分钟扫一次
timestamp超过 10 分钟的项,或按 size > 10000 就删最老的 10%
数据库唯一索引不是可选项,而是兜底铁律
所有中间层幂等控制都可能失效:Redis 故障、缓存击穿、客户端绕过 header、网络重传……DB 层才是最后一道不可绕过的防线。
- 订单表必须建联合唯一索引,如
UNIQUE (user_id, idempotency_key),字段别选太宽(比如全量 body),影响写性能 - 插入失败后,别直接抛错;要用
pgx.ErrCodeUniqueViolation(PostgreSQL)或mysql.MySQLError.Number == 1062(MySQL)精准识别冲突 - 捕获到唯一约束错误后,必须查一次 DB 确认是否真已存在——不能假设“报错=已成功”,因为可能 DB 写入成功但响应丢失
- 别在事务中间校验唯一性,必须放在最外层;否则 DB 成功但 Redis 失败,下次请求会因 key 存在被误拒
真正难的不是加一层缓存或一个索引,而是把幂等状态和业务执行生命周期对齐:请求进来时锁住、执行中保持 pending、成功后标记 success、失败后留 failed——布尔值撑不住,三态状态机才是生产环境能落地的底线。











