sync.map仅适用于单机、qps几百的轻量幂等场景,不支持自动过期、不跨进程、无分布式能力;高可用幂等必须依赖redis原子setnx+ex、db唯一索引兜底三层结构。

sync.Map 不是分布式方案,它只在单进程内有效;真正支撑多实例、高可用的幂等性,必须依赖外部存储 + 原子操作 + DB兜底三层结构。
Redis.SetNX 必须带 NX 和 EX,不能拆成两步
常见错误是先 Exists 再 Set,或 SetNX 成功后补 Expire。这两个做法都会在并发下漏判:两个请求同时查到 key 不存在,都写入,重复执行就发生了。
正确姿势是原子命令:
rdb.Set(ctx, key, value, ttl).AddArgs("NX", "EX", "300")
或直接调用封装好的 SetNX 方法(v9+ 客户端已内置)。注意:ttl 要大于接口最长耗时 + 网络抖动余量,比如支付回调卡住 45 秒,ttl 至少设 60 秒。
- value 别存空字符串,建议填
X-Idempotency-Key或trace_id,方便日志对齐 - key 构造要含业务上下文,例如
"idempotent:order_create:" + userID + ":" + idempotencyKey,避免不同接口冲突 - Redis 连接失败时,必须返回
503 Service Unavailable,绝不能 fallback 到“放行”
数据库唯一索引不是可选项,而是不可绕过的兜底
所有中间层(Redis、内存缓存、消息队列去重)都可能失效:缓存穿透、Redis 故障、客户端绕过 header、网络重传……DB 层才是最后一道防线。
订单类场景必须建联合唯一索引,例如:
UNIQUE (user_id, idempotency_key)
插入失败后,不能直接抛错或静默返回 success。必须精准识别冲突错误:
- PostgreSQL:检查
pgx.ErrCodeUniqueViolation - MySQL:判断
mysql.MySQLError.Number == 1062
捕获到唯一冲突后,要查一次 DB 确认是否真已成功——因为 DB 插入失败 ≠ 业务未执行(比如发了短信、扣了库存但最后一步写库崩了)。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
别用 sync.Map 做分布式幂等,它连“跨 goroutine”之外的事都管不了
sync.Map 只适合单机、QPS 几百、无扩容需求的极简场景。它不支持自动过期、不跨进程、重启即丢,更谈不上分布式一致性。
如果误把它当分布式方案用,会遇到这些翻车点:
- key 构造混了
context或临时变量(比如从req.URL.Query().Get("t")拿时间戳),导致 key 污染、误判 - 缓存值只存
bool,命中后没法返回原始响应体和状态码 - 没配定时清理,
timestamp超过 10 分钟的项越积越多,map size 爆涨 - 多个 handler 共享一个
sync.Map实例没问题,但若每个 handler 都 new 一个,就彻底失去去重意义
真要用它,缓存结构必须是完整响应体:{status: "success", result: json.RawMessage, timestamp: time.Time},且写入前必须 Load,命中就直接返回,绝不跳过校验。
幂等状态机必须是三态,不能只靠 is_executed 布尔字段
只存一个布尔值,无法区分“正在执行中”和“已成功”,会导致并发请求卡死或错误返回。
必须用三态(pending/success/failed)配合 CAS 更新:
- 首次请求:尝试插入
status = "pending",成功则执行业务逻辑 - 执行完成后:用
UPDATE ... WHERE status = "pending"将状态改为success或failed - 重复请求:查到
success直接返回缓存结果;查到failed原样返回错误;查到pending可选择等待或快速失败
这个状态机必须落库或 Redis,不能只存在内存里——服务重启、节点扩缩容都会让状态丢失,幂等语义就断了。
关键点始终落在「持久化」和「原子性」上:Redis 提供高性能原子占位,DB 提供强一致最终保障,而任何把状态存在内存、靠 HTTP 方法语义、或信任客户端不重试的设计,上线后大概率会在某个凌晨三点准时出问题。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










