跳到主要内容

幂等键与安全重试

幂等键让客户端对同一个业务请求重复发送时,服务端返回同一执行结果,而不是再次产生副作用。它适合解决响应丢失和超时后的结果不确定,不能把两个不同业务动作合并成一个。

1. 幂等键标识一次业务意图

客户端为创建订单、支付或退款请求生成难以碰撞的唯一 key,并在结果未知时复用它。用户再次主动创建一笔新订单,应使用新 key。

服务端通常按“调用主体 + 接口 + 幂等键”划分作用域,避免不同租户碰巧使用相同字符串互相影响。key 还要绑定请求内容摘要,同一 key 携带不同金额或参数时应返回冲突,而不是复用旧结果。

2. 服务端原子占用幂等键

幂等记录可以包含:

key, principal, request_hash, status, resource_id, response, expires_at

第一次请求通过数据库唯一约束原子创建 PROCESSING 记录。并发重复请求发现已有记录后,根据状态返回已完成结果、等待有限时间,或提示客户端稍后查询。

先查询 key 不存在、再插入记录会产生竞态。唯一约束或等价的原子 put-if-absent 才能决定唯一执行者。

3. 幂等记录与业务结果保持一致

如果业务结果存入同一个数据库,应把幂等记录、业务变化和最终状态放进同一本地事务。事务提交后,重复请求可以直接返回保存的 resource ID 或响应摘要。

若先把幂等记录标记完成,业务事务随后失败,客户端会得到虚假成功;若业务提交后记录未完成,重复请求可能再次执行。跨数据库或外部系统时,需要状态机、Outbox 和结果核对处理这个窗口。

4. 正在处理的重复请求不能无限等待

两个相同请求并发到达时,后一个可能看到 PROCESSING。可选择:

  • 短时间等待首个结果,但受调用 deadline 限制。
  • 返回 202 和状态查询地址。
  • 返回冲突或“处理中”错误,由客户端稍后查询。

执行者崩溃会留下悬挂状态。记录应有租约、心跳或可恢复工作流,并根据外部结果决定接管、补偿或人工处理,不能简单到期后无条件再执行支付。

5. 返回结果要保持兼容

服务可以保存完整响应,也可以保存资源 ID 后重新读取当前表示。完整响应能准确重放首次结果,但占用空间,并可能包含过期 header 或敏感数据;重新读取可能返回资源之后的变化。

应在 API 契约中说明重复请求返回首次响应还是当前资源。错误也要区分:参数校验失败通常无需长期占用 key,已经开始外部副作用的失败则需要保留状态。

6. 保留时间覆盖最大重试窗口

幂等记录不能立即删除。TTL 至少覆盖客户端、消息系统、人工补单和网络中间层可能再次发送的最长时间。支付和审计场景可能需要长期保留业务唯一号,而不是仅依靠短 TTL 缓存。

清理后,同一旧 key 再次出现会被当成新请求。服务要么接受这个风险,要么让业务资源本身的唯一约束继续阻止重复。

7. 条件状态转换提供第二层保护

即使已有幂等 key,业务写入也应尽量表达合法状态:

UPDATE payment
SET status = 'CAPTURED'
WHERE payment_id = ? AND status = 'AUTHORIZED';

它防止不同 key、消息重复和人工操作绕过入口后重复推进状态。幂等键解决请求身份,状态机和唯一约束保护领域不变量。

8. 客户端只在结果未知时复用 key

网络超时、连接 reset 和 5xx 可能表示结果未知,客户端在剩余 deadline 内按退避策略重试同一 key。明确的参数错误应修正内容并使用新的业务请求,而不是在原 key 下改变 payload。

客户端还应提供按业务 ID 查询结果的能力。总预算耗尽后,停止自动重试并查询状态,通常比继续制造请求更安全。

9. 常见问题

9.1 数据库唯一索引和幂等键有什么关系

唯一索引可以原子阻止重复业务结果,是实现幂等的重要工具。幂等键还保存请求身份、处理状态和可重放结果,让客户端在响应丢失后获得确定答案。两者可以同时使用。

9.2 key 存在 Redis 就足够吗

如果 Redis key 与数据库业务提交不在同一原子边界,仍有先后故障窗口;Redis 淘汰或故障转移也可能丢记录。关键写入通常把幂等状态放进权威数据库,Redis只用于加速查询。

10. 面试题

10.1 如何设计一个支持安全重试的支付接口

出现公司:美团

考察重点

  • 幂等键作用域、请求摘要和原子占用。
  • 业务事务、处理中状态和外部副作用。
  • TTL、状态查询与条件状态转换。

相关内容:第 1 节“幂等键标识一次业务意图”至第 8 节“客户端只在结果未知时复用 key”。

参考回答

客户端为一次支付意图生成幂等 key,结果未知时始终复用。服务端按商户、接口和 key 建唯一记录,并绑定请求摘要;同 key 不同金额直接拒绝。第一次请求原子创建处理中记录,数据库内的支付状态和幂等完成状态放在同一本地事务,重复请求返回已有资源或结果。

外部支付调用无法与本地事务原子提交时,要用持久化状态机、稳定下游请求 ID、结果查询和补偿处理崩溃窗口。处理中请求不能无限等待,返回状态查询入口;记录保留时间覆盖最大重试和补单窗口。支付状态还用条件更新和业务唯一约束保护,避免不同 key 绕过幂等入口。