API 幂等键状态机:请求指纹、处理中与结果重放

API 幂等键状态机:请求指纹、处理中与结果重放原创封面

幂等保护的是同一业务意图的重复到达

移动网络、网关超时和客户端重启都会让调用者不知道首次请求是否完成。创建、扣费或导入接口若每次重试都执行新动作,就会产生重复资源。幂等键让客户端为一次业务意图提供稳定身份,服务端保存该键的处理状态和结果,使重复请求能够返回已有进度。它不等于所有 POST 自动安全,也不允许客户端把一个键永久复用于不同订单。

首先明确哪些端点支持键、键的作用域和有效期。作用域通常包含认证主体、HTTP 端点和业务环境,防止两个用户或测试生产之间碰撞。客户端在首次尝试前生成键,所有网络重试保持不变;用户明确发起第二次业务动作时才生成新键。服务端必须把键视为不可信输入,限制长度和字符集,日志只记录哈希或受控摘要。

请求指纹阻止同键承载不同正文

只对 idempotency_key 建唯一约束会让误复用返回第一笔交易结果,调用者可能以为第二个不同请求成功。首次受理时应计算规范化请求指纹,包含真正影响语义的路径参数、查询和正文。相同作用域、相同键但指纹不同必须返回冲突,不能覆盖旧记录或启动新动作。认证令牌、追踪头和无语义字段不进入指纹,否则令牌轮换会制造假冲突。

规范化要由服务端基于解析后的数据执行,而不是直接哈希原始 JSON 字节;对象键顺序和无意义空白不应改变指纹。同时不能随意删除默认值,省略字段与显式 null 是否等价由业务 schema 决定。上传文件可使用验证后的内容哈希与元数据组合,但计算前仍要限制字节。指纹算法和 schema 版本保存在记录中,升级算法不能让旧键突然被当作新请求。

数据库唯一约束裁决并发首次请求

两个相同请求可能在同一毫秒到达不同实例,因此“先查询,没有就插入”不安全。幂等表对 actor_scope、endpoint 和 key 建唯一约束,首次请求在短事务尝试插入 PENDING 与指纹。唯一冲突者读取既有记录并按状态响应。不要用应用进程锁承担裁决,它覆盖不了多实例和重启;也不要在持有数据库锁时执行长外部调用。

创建幂等记录与本地业务资源如果能在同一数据库事务完成,就一起提交并直接保存 COMPLETED 与资源 id。需要外部动作时,首次事务只建立所有权和稳定 operation_id,然后在事务外调用,并用版本条件确认结果。PENDING 不是失败,它表示另一个执行者可能仍工作;重复请求可返回处理中和查询位置,或在很短预算内等待,但不能并发再执行。

状态机区分处理中、结果未知和可重试失败

实用状态通常包括 PENDING、IN_PROGRESS、COMPLETED、RETRYABLE_FAILURE 和 TERMINAL_FAILURE,并记录租约、尝试次数与最后阶段。外部请求超时后是结果未知,不能直接重置为可执行;先用 operation_id 查询远端,能确认未受理才重试。远端已成功则保存其资源标识并完成本地记录。无法查询且副作用高风险时进入人工对账,而不是用重试概率赌一次。

工作者崩溃由租约接管,但接管更新必须匹配版本与过期时间。旧工作者迟到返回时,提交条件影响零行,不得覆盖接管者结果。确定性校验失败可以保存终止状态并重放相同错误摘要;429、短暂 5xx 和数据库不可用使用有上限退避。错误响应是否缓存要按语义决定,不能把一次瞬态 503 在整个键有效期内永久重放。

结果重放保存必要信息而非敏感响应全集

COMPLETED 记录要足以构造与首次调用语义一致的响应,通常保存 HTTP 状态、资源 id、版本和少量稳定字段。完整响应可能包含短期签名 URL、个人数据或随后变化的展示内容,不适合长期复制。可以重放资源引用并重新读取当前可见表示,但要明确首次与重试是否允许返回不同展示字段。任何方式都必须再次执行当前调用者授权,不能仅凭知道幂等键读取结果。

若原响应创建了只能显示一次的秘密,幂等设计必须在产品层决定:加密保存到有效期、首次后只返回已创建状态,或禁止自动重试。不能为了“完全相同响应”把明文令牌长期放进幂等表。日志记录 operation_id 和响应分类,不打印正文。删除用户数据时还要清理幂等记录中的派生敏感字段,同时保留最低限度的防重复标识与合规审计。

过期与清理覆盖最大重试和恢复窗口

TTL 过短会让迟到重试在记录删除后重新执行,过长则增加存储和键碰撞风险。保留期应覆盖客户端最大离线重试、消息重投、人工支持和灾难恢复可能带来的旧请求。不同业务损失不同,支付键可能需要比普通搜索导出更长。清理只处理终态且超过期限的记录,PENDING 或结果未知任务不能被时间一到直接删除。

批量清理按稳定主键限制行数,观察锁、WAL 与表膨胀。若业务资源已经删除,重放可返回原操作完成但资源不再存在的明确语义,不能悄悄重新创建。多地域系统还要考虑请求被路由到尚未复制幂等记录的区域;需要强一致路由、全局裁决或区域绑定,不能假设异步副本足以阻止同时执行。

测试从提交点两侧注入断网

集成测试让数十个并发请求使用相同键和相同正文,数据库最终只能有一个业务资源和一条操作记录,其余响应指向同一结果。随后用相同键发送不同正文,必须稳定冲突且旧结果不变。测试进程在创建幂等行前、业务提交前、提交后响应前、远端成功后本地确认前退出,每个恢复路径都验证副作用数量。

时钟测试覆盖租约到期、TTL 边界和迟到旧工作者,使用可控数据库时间而不是 sleep。权限测试确认另一个用户即使获得键也看不到结果。故意让审计或结果保存失败,事务内变更全部回滚;外部已成功场景保留结果未知并可对账。性能测试观察唯一索引热点、PENDING 等待和清理压力,成功率再高也不能接受偶发重复扣费。

协议文档让客户端与服务端共同守约

API 文档说明键格式、适用端点、作用域、保留期、相同键不同请求的状态码,以及处理中时如何查询。客户端 SDK 只对明确支持的操作自动重试,并保持同一正文与键;用户修改表单后必须视为新意图。网关转发时不能丢弃幂等头,也不能在服务端尚运行时自行换键重放。限流仍按调用者执行,幂等键不是绕过配额的凭证。

上线从低风险创建接口灰度,监控首次、重复命中、指纹冲突、悬挂和接管数量。指纹冲突增多可能是客户端错误复用,悬挂增多可能是租约或下游异常,不能统一归为正常重试。运维后台提供按 operation_id 查询和受控修复,但人工恢复也必须遵守版本状态机并写审计。幂等真正解决的是不确定交付,不是把任何失败都变成无条件重复。

网关、异步任务和多地域必须共享同一操作身份

反向代理可以转发 Idempotency-Key,却不应替客户端在每次重试随机生成,也不能只在网关内存缓存响应。网关超时后源服务可能继续处理,代理若换键重发会制造第二次动作。请求标识、追踪标识和幂等键用途不同:前两者可以每次尝试变化以便诊断,幂等键在同一业务意图的整个生命周期保持稳定,并一路传入下游 operation_id。

长任务接口首次请求创建操作记录和任务 id,返回 202 与查询地址。重复相同键返回同一任务,不再排队一份;任务最终结果写回幂等记录。取消也使用版本状态机,只有尚未进入不可逆阶段才承诺取消。用户取消 HTTP 请求不等于取消后台任务,服务端必须明确两种语义,避免客户端断开后重试创建新任务。

多地域部署若各区域独立接受首次键,异步复制窗口会产生双执行。可将同一主体稳定路由到主区域,使用全局强一致存储裁决,或让业务资源自身的全局唯一约束承担最后防线。方案应明确区域故障时是暂时拒绝高风险写入还是切换所有权;“最终会复制”只会最终发现重复,不能阻止重复。

下游服务不支持幂等时,本服务保存外部调用账本,记录尚未调用、结果未知、远端资源已确认和本地完成。客服工具按 operation_id 对账,不能简单把 PENDING 改为失败。指标区分重复命中、正文冲突、租约接管与远端未知,帮助定位客户端误用和依赖故障,而不是把所有命中都当成节省请求的好消息。

实现片段

UNIQUE (actor_id, endpoint, idempotency_key)

一手参考资料

评论 · 0

还没有评论留下第一句经过思考的话。

游客评论需审核。注册后可直接公开,无需审核。

SHARE / 分享

分享这篇文章

WECHAT / 微信

用微信扫一扫

在手机微信中打开文章后,再从微信右上角分享给朋友或朋友圈。