直接用redis+uuid做幂等会漏判重复请求,因其无法处理“请求中途失败但业务已执行”场景;必须将幂等键与业务语义绑定(如user_id:order:create),含用户id、订单号等标识,并设合理过期时间、原子写入及主动清理机制。

为什么直接用 Redis + UUID 做幂等会漏判重复请求
因为没处理「请求中途失败但业务已执行」的场景。比如客户端发了 POST /order,服务端收到、生成订单、写库成功,但网络抖动导致响应没发回去;客户端重试,新请求带着另一个 uuid 过来,Redis 里查不到,就又建了一单。
真正要拦住的是「同一业务意图」,不是「同一 token」。所以必须把幂等键和业务语义绑定,比如 user_id:order:create:20240715 或 pay_req_id:123456,而不是无意义的随机 uuid。
- 幂等键必须含业务标识(用户 ID、订单号、支付流水号等),不能只靠客户端生成的 token
- Redis key 生效时间要覆盖整个业务链路耗时,建议设为
30m而非5m,避免超时后重试失效 - 写入 Redis 必须和主业务逻辑在同一个事务里(如用 Redis Pipeline + DB 事务兜底),否则可能写 Redis 成功但 DB 失败,下次请求被误拒
去重表方案中唯一索引怎么设才不拖慢写入
MySQL 去重表常见错误是建了个宽索引,比如 UNIQUE KEY (req_id, user_id, timestamp),结果每次 INSERT 都要扫三列做校验,QPS 上不去。实际只需锁定「业务不可重复维度」。
例如支付回调接口,核心约束是「同一笔支付流水只能成功通知一次」,那唯一索引就该是 UNIQUE KEY (pay_no);如果是用户提交实名认证,约束是「同一身份证号在同一流程中不可重复提交」,索引就是 UNIQUE KEY (id_card, flow_type)。
- 索引字段越少、类型越简单(优先用
VARCHAR(32)而非TEXT),INSERT 冲突检测越快 - 避免在去重表里存冗余字段(如完整请求体、日志),只保留判定必需的字段 +
created_at - 如果并发极高,可考虑分表:按
pay_no % 16落到 16 张子表,缓解单表锁竞争
gRPC 接口如何透传幂等上下文而不改业务方法签名
gRPC 默认不支持类似 HTTP 的 header 透传,但可以用 metadata.MD 在 client 和 server 间带幂等键。关键是别让每个 handler 都手动解析 —— 要用拦截器统一提取并注入 context。
示例:客户端加 metadata
ctx := metadata.AppendToOutgoingContext(context.Background(), "idempotency-key", "user_123:withdraw:20260701") resp, err := client.Withdraw(ctx, req)
服务端拦截器提取后塞进 context:
func IdempotencyInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, ok := metadata.FromIncomingContext(ctx)
if ok {
keys := md.Get("idempotency-key")
if len(keys) > 0 {
ctx = context.WithValue(ctx, "idempotency_key", keys[0])
}
}
return handler(ctx, req)
}
- 别把幂等键存在
context.WithValue的 string key 里(易冲突),定义专用 type:type idempotencyKey string,再用context.WithValue(ctx, idempotencyKey("key"), val) - 拦截器里不做 Redis 查询或 DB 插入,只提取和传递;具体校验逻辑交给业务 handler 自己决定何时触发
- 如果服务同时暴露 HTTP 和 gRPC,HTTP 中间件和 gRPC 拦截器要共用同一套幂等键解析规则,比如都从
X-Idempotency-Keyheader 或 metadata 提取
幂等框架必须支持失败回滚的两种情况
很多团队只做了「拦截重复请求」,却忘了「已执行但未返回」的请求怎么清理。这会导致 Redis key 或去重表记录长期占位,最终撑爆内存或锁表。
真正要处理的不是「是否重复」,而是「上次请求到底执行到哪一步了」。
- 对「写 DB + 发消息」类操作,必须在事务提交后,立即用 Lua 脚本原子性地删 Redis key 或更新去重表状态字段(如
status ENUM('pending', 'success', 'failed')) - 对「调第三方支付」类操作,要设计补偿任务:扫描所有
status = 'pending'的记录,查支付平台确认结果,再更新本地状态并释放幂等锁 - Redis 方案里别用
EXPIRE依赖 TTL 自动清理,TTL 到期前若业务卡住,key 就一直挂着;应主动标记「已完成」并删除
最常被跳过的环节是:没有给幂等记录加「最后活跃时间」字段,导致无法识别僵尸记录。上线前务必加上 updated_at 并配定时 job 清理超时项。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











