4. 幂等性设计原则:请求去重、Idempotency Key 设计、责任划分
这章我们聊聊幂等性落地的三个核心支柱。说白了就是三件事:怎么识别重复请求、用什么东西来标记、谁该管哪一段。
我刚开始搞 Secrets Manager 接口的时候,以为幂等就是加个去重表就完事了。结果上线第一天就被打脸——客户端重试机制和 Server 端去重逻辑打架,导致同一把密钥被创建了两次。嗯,那次之后我才真正理解了这三件事必须拆开设计,再拼回一个闭环。
4.1 请求去重:不是“存一下”就完了
请求去重的本质是:同一幂等键,同一业务结果,只执行一次。但实现上有几个坑。
4.1.1 去重时机与状态机
我习惯把每个幂等请求的生命周期拆成三步:
- 收到请求:检查幂等键是否已存在。若存在且已成功,直接返回缓存结果;若存在且处理中,返回“正在处理”或等待;若不存在,写一条“处理中”记录。
- 执行业务:在事务内更新幂等记录状态为“成功”,同时保存结果。
- 返回响应:下游重试时再次命中步骤1,直接返回之前的结果。
看个简化版的实现思路:
// 伪代码:幂等去重中间件
func IdempotencyMiddleware(r *http.Request) {
idempotencyKey := r.Header.Get("Idempotency-Key")
if idempotencyKey == "" {
// 非幂等请求放行
return
}
record, err := db.GetIdempotencyRecord(idempotencyKey)
if err == nil {
switch record.Status {
case "SUCCESS":
// 直接返回已缓存的响应
writeResponse(r, record.Response)
return
case "IN_FLIGHT":
// 返回 409 或等待
writeError(r, 409, "Request already in progress")
return
}
}
// 插入 IN_FLIGHT 记录,配合唯一索引防并发
err = db.InsertIdempotencyRecord(idempotencyKey, "IN_FLIGHT")
if err != nil { // 唯一索引冲突说明并发写入
writeError(r, 409, "Duplicate request")
return
}
// 进入业务逻辑...
}
注意:幂等记录的状态必须从“处理中”转换到“成功”或“失败”。如果一直处于“处理中”,重试的客户端会永远卡住。我曾经犯过这个错,结果客户端把超时时间设成了 30 秒,服务端处理用了 5 秒,但状态没及时推进——重试全堵死了。
4.2 唯一标识符(Idempotency Key)设计
Idempotency Key 就是整个机制的“钥匙”。钥匙丢了或者重复了,整个体系就崩了。
4.2.1 谁来生成?
这是个关键选择。我习惯把决策权交给客户端。为什么?
- 客户端最容易保证全局唯一(配上 UUID v4 或 ULID)。
- 服务端如果自己生成,重试时客户端拿不到同一个 Key,幂等性就失去了意义。
- 客户端可以自主控制重试策略(超时、重试次数、间隔)。
4.2.2 格式与存储
| 维度 | 推荐做法 | 不推荐做法 |
|---|---|---|
| 格式 | UUID v4 / ULID(128位随机) | 时间戳+自增ID(容易冲突且可预测) |
| 长度 | 36字符(含连字符) | 过长或过短 |
| 传输方式 | HTTP Header: Idempotency-Key |
放在 Body 里(中间件解析困难) |
| 存储类型 | Redis + TTL 或 数据库唯一索引 | 只存内存不持久化(服务重启全丢失) |
一个小建议:如果使用了 ULID,它天然带时间排序属性,可以用来做过期清理的索引。我在项目中用 Redis 存,TTL 设置成 24 小时,配合后台定时扫库清理。
4.3 服务端与客户端责任划分
这个划分如果模糊,你和同事就会互相甩锅。我画过一张边界图,你看一下:
你看这张图,左边客户端只负责生成 Key、发请求、处理超时重试;右边服务端只负责校验、存储、状态机、过期清理。中间那条线就是契约——客户端保证 Key 全局唯一,服务端保证 Key 幂等。
3.3.1 典型边界场景
我曾遇到过两个典型纠纷:
- 场景一:客户端没有传 Key。服务端是直接拒绝,还是自动生成一个?我建议直接拒绝并返回 400。因为服务端自动生成的 Key,客户端重试时根本拿不到,幂等性等于没做。
- 场景二:客户端重试时服务端返回 5xx。服务端该不该把幂等记录设为失败?我的做法是:如果业务还没执行(比如网络超时),不更新记录;如果业务已执行但响应发送失败,那记录已经标记为成功,重试直接返回缓存结果。这需要业务层做“幂等写入”保证。
核心原则总结
- 幂等 Key 由客户端生成,服务端只消费和存储。
- 服务端必须用唯一索引或 Redis 原子操作防止并发。
- 幂等记录状态机必须闭环:处理中 → 成功/失败。
- 服务端返回的响应必须与第一次成功执行的结果完全一致(包括状态码和响应体)。
- 幂等记录的 TTL 应覆盖最长的客户端重试窗口 + 时钟偏差。