重复 API 提交:在添加键之前设计幂等性契约
定义调用方、载荷、原子声明和重放策略,使重试具有可预测的应用结果。
本文内容
简明答案
对于重试的订单提交,应在同一认证调用方和同一逻辑载荷的情况下复用同一个键。应用程序可以原子化地声明该组合,并保留其完成结果以供重放。使用相同键但载荷已更改的请求应收到文档化的应用冲突。唯一数据库行用于协调声明;但它本身并不能保证支付、邮件或其他外部效果恰好发生一次。
将 HTTP 语义与应用契约分开
幂等操作是指重复执行时与执行一次时具有相同的预期服务器效果。响应不必完全相同,定义也能成立。POST 端点仅仅因为客户端发送了额外的头部,并不自动获得安全重试契约。服务器必须实现并文档化如何解释该键、它覆盖哪些操作,以及重试如何与认证和存储状态交互。
将键的范围限定到受信任的调用方
在假设示例中,键 k1 属于一个认证调用方和一个订单创建操作。另一个调用方使用 k1 时,不应收到第一个调用方的结果。应从受信任的认证获取调用方身份,而不是从自由提供的载荷字段获取。定义键是在操作之间共享命名空间,还是在特定端点范围内,并在数据库唯一性规则中保留该范围。
指定载荷等价性
同一个键应代表相同的逻辑提交。定义哪些字段属于操作,以及应用程序如何比较它们,例如使用规范化表示或在文档化规则下的摘要。原始 JSON 字节可能不同但表示相同数据,因此字节相等是一种策略选择。反之,排除金额等重要字段可能导致将不同订单错误地识别为等价。
走一遍构造的重试
假设调用方 A 使用键 k1 提交一个载荷,请求两个单位的项目 X。第一次成功请求存储一个订单结果。A 使用 k1 和等价载荷重试时,在此提议契约下返回保留的结果。使用 k1 但请求三个单位的请求因载荷不匹配而被拒绝。这是一个设计说明,而非断言每个现有 API 都使用相同的状态码或重放行为。
原子化声明键
先检查后插入的序列可能发生竞争:两个请求都可能看到没有现有键。数据库在所选调用方、操作和键范围上的唯一性约束可以强制执行单个存储声明。PostgreSQL 的 INSERT ON CONFLICT 可以帮助协调插入。应用程序仍需解释它是拥有新声明还是找到了现有声明,并且不能让每个失败的请求都去执行受保护的操作。
表示处理中和已完成状态
存储足够的状态以区分仍在处理的请求和可以重放结果的请求。定义并发重试在第一次尝试运行时如何响应:等待、文档化的临时响应或重试指令都是可能的策略。尽可能与数据库更改一致地持久化已完成状态及其结果。不要将任何键行的存在视为订单已成功完成的证据。
处理外部效果和崩溃窗口
支付提供方或邮件服务不会自动参与数据库事务。在外部效果之后、存储完成结果之前发生崩溃,会产生恢复问题。持久化出站表、提供方支持的去重或显式协调可能构成解决方案的一部分,具体取决于效果。每种都有自己的契约。仅仅将本地键插入包装在事务中,并不能建立跨系统的恰好一次行为。
文档化保留和恢复限制
说明键和结果保留多久、重放返回什么以及过期后发生什么。删除记录可能允许后续使用相同键的请求成为新提交。还要定义对放弃的处理状态的恢复,以及失败的尝试是否可重用。这些选择影响正确性和存储。客户端必须能够区分安全重试和创建新的逻辑操作。
检查清单
- 仅对相同的逻辑操作复用键。
- 将查找范围限定到认证调用方。
- 定义载荷等价性及不匹配时的行为。
- 使声明具备原子性和唯一性。
- 为外部效果和过期键规划恢复策略。
适用范围
本指南描述了一个假设的应用契约,而非完整的服务器实现,也不是通用的 Idempotency-Key 标准。数据库唯一性和 HTTP 幂等性本身并不能保证外部副作用恰好发生一次。