不能只校验idempotency-key请求头,因服务重启、分布式无共享、响应丢包等场景会导致重复执行;需结合redis原子setnx、数据库唯一索引与三态状态机协同保障幂等性。

为什么不能只校验 Idempotency-Key 请求头
只读取 http.Header.Get("Idempotency-Key") 并做简单去重,看似省事,实则漏掉关键场景:服务重启后内存清空、分布式节点间无状态共享、下游调用成功但响应丢包导致客户端重试。它拦得住“请求重放”,拦不住“业务重复执行”。
- 客户端超时重试时,相同
Idempotency-Key可能落到不同服务实例上 - 若服务端用本地
map或sync.Map缓存 key,扩容或崩溃即失效 - HTTP 层返回 200 不代表 DB 写入成功,更不保证消息已发、余额已扣
redis.SetNX 必须带 EX 和原子性保障
用 github.com/go-redis/redis/v9 的 SetNX(ctx, key, value, ttl) 是最常用落地方式,但常见错误是忽略过期时间或拆成两步操作。
- 必须设
ttl(如45 * time.Second),要大于接口最大耗时(含下游超时),否则 key 过早失效,失去幂等性 - 绝不能先
GET再SET—— 中间存在并发窗口,两个请求同时读到空值,都会进入业务逻辑 - value 建议填
trace_id或request_id,方便排查冲突来源,而不是空字符串或固定值 - 连接池配置不当(如 timeout 太短)会导致 SetNX 调用失败,整个幂等逻辑被跳过
数据库唯一索引不是替代方案,而是兜底防线
Redis 只降低冲突概率,不能 100% 拦住重复;唯一索引才是最终一致性保障,但它不能单独用,也不能代替状态机。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 订单表应建联合唯一索引,如
UNIQUE INDEX idx_user_id_idemp_key (user_id, idempotency_key),字段宽度要可控,别用全量 body 哈希 - 捕获错误时,MySQL 看
errno 1062,PostgreSQL 看pgx.ErrCodeUniqueViolation,不能泛 catch 所有 error 后静默返回 success - 插入失败 ≠ 业务已完成 —— 必须查库确认该记录是否真已存在、状态是否为 success,再决定返回缓存结果还是重抛 error
- 不要用唯一索引替代三态状态机:插入失败只能说明“写入被拒”,无法区分是 pending 中还是已成功
状态机必须是三态且 CAS 更新
只存一个 is_executed bool 字段,在高并发下会出问题:两个请求同时查到 false,都往下走;或一个在执行中,另一个误判为已完成。
- 状态至少为
"pending"/"success"/"failed",推荐加"canceled"构成四态 - 首次请求用原子写入(如 Redis
SET idempotent:{key} pending EX 30 NX)标记为 pending - 业务执行完,用 CAS 更新:
UPDATE orders SET status = "success", result = ? WHERE idempotency_key = ? AND status = "pending" - 重复请求查到
"success"就直接返回缓存响应(含完整 body 和 status code);查到"failed"则原样返回上次错误;查到"pending"可选择等待或快速失败
Redis 的原子性、DB 的最终一致性、状态机的中间态,这三者缺一不可。最容易被忽略的是「查到 pending 后怎么办」——既不能立刻返回错误(可能只是慢),也不能无限等待(阻塞线程)。实际落地时,这个等待策略和超时阈值,得贴着你的业务 SLA 来定。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










