redis.setnx是最稳妥的接口幂等性实现方式,因其原子性地完成设值与首次判断,避免get+set的竞态问题;key建议为"idempotent:{user_id}:{req_id}",过期时间设5–15分钟,成功后删除;需结合状态机校验,而非仅依赖req_id或数据库唯一索引。

用 redis.SetNX 做请求唯一标记最稳妥
接口幂等性不是靠“重试逻辑”或“前端按钮防点”撑起来的,核心是后端能识别“这请求我是不是已经处理过”。redis.SetNX(即 SET key value NX EX seconds)是最常用也最可靠的判断手段——它原子性地完成“设值 + 判断是否首次”,不存在竞态窗口。
常见错误是用 GET + SET 两步走:先查 key 是否存在,再决定是否写入。这在并发下必然漏判,两个请求几乎同时查到空,都写入,就重复了。
- key 建议拼成
"idempotent:{user_id}:{req_id}",其中req_id由前端传入(如 UUID),服务端不生成 - 过期时间别设太长,一般 5–15 分钟足够;太短可能误判重试,太长会堆积无效 key
- 务必在业务逻辑执行成功后才删 key(或让它自然过期),不能一进来就删——否则失败重试时就拦不住了
HTTP 方法语义不能替代幂等实现
很多人以为 PUT 或 DELETE 天然是幂等的,就不再加控制。这是错的:PUT /api/order/123 确实该幂等,但如果你的实现里没校验“这次提交的数据和上次是否一致”,而是直接覆盖写库,那两次相同请求可能因中间状态变更(比如库存被其他请求扣减)导致结果不同。
更危险的是把 POST 接口硬说成“幂等”,只靠文档约定。实际调用方不会读文档,SDK、重试机制、用户刷新都会触发二次提交。
-
GET和HEAD理论上应只读,但若混入写操作(如埋点计数),必须自己加幂等控制 -
PUT幂等的前提是:你真按 RFC 语义实现——用完整资源表示做替换,且不依赖外部状态(如当前库存) - 不要信任客户端传来的
If-None-Match或ETag做幂等判断,它们解决的是缓存一致性,不是业务重复提交
数据库唯一索引只能兜底,不能当主方案
加个 UNIQUE INDEX (req_id) 看似简单,但它只在插入失败时报 ERROR: duplicate key value violates unique constraint,而这个错误发生时,业务逻辑可能已部分执行(比如发了消息、调了下游),回滚成本高、边界难控。
它适合做第二道防线,比如在 redis.SetNX 成功后,再往 DB 插一条幂等记录;但如果 DB 写失败,redis key 还得手动清理,否则后续请求全被拦住。
- 唯一索引字段别只用
req_id,加上business_type或tenant_id避免跨业务冲突 - 捕获
pgerr.CodeUniqueViolation(PostgreSQL)或对应 MySQL 错误码时,要明确区分是幂等冲突还是其他唯一约束失败(如用户名重复) - 别在事务外建索引再插入——索引建好前的窗口期,重复请求仍可写入
带状态流转的业务怎么保幂等
订单支付、工单审核这类接口,不是“有或无”的问题,而是“从 A 状态能否转到 B 状态”。这时光靠 req_id 拦不住非法状态跃迁(比如已支付的订单又被点一次支付)。
必须把状态机规则编码进幂等检查:先查当前状态,再判断本次请求是否允许该跳转,最后才更新。Redis 里存的不能只是“是否来过”,还得是“当时的状态快照”。
- 推荐用
HSET idempotent:{req_id} status "paid" version 123,后续请求比对status和预期起点状态 - 如果状态更新涉及多张表,用数据库行锁(
SELECT ... FOR UPDATE)比 Redis 更可靠,但要注意锁粒度和超时 - 千万别把“最终状态”当判断依据——比如支付回调里查订单已是“paid”,就直接返回成功;万一第一次支付成功但通知丢了,第二次回调其实是补救,该走正常流程
真正难的不是加 redis 或建索引,是理清每个接口背后的状态契约:谁有权改、改之前必须满足什么、改之后要触发哪些副作用。这些不写进代码注释,光靠命名或文档,三个月后你自己都认不出来。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











