#TypeScript#DevOps

HTTP 幂等键正确性:Payload 绑定、并发接受点与未知结果恢复

Idempotency-Key 相同不代表请求相同,缓存一次响应也挡不住并发双写。本文定义操作身份、规范化 payload 摘要、原子占位、结果重放和未知副作用对账合同。

本文要点

  • 主题聚焦:TypeScript、DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年4月18日·更新于 2026年7月20日

为 POST 增加 Idempotency-Key 只能表达客户端希望重放同一操作,不能证明两次请求确实相同。若服务端只按键缓存响应,攻击者或有 bug 的 SDK 可以先用键 K 创建金额 10 的订单,再用同一个 K 提交金额 100;若后一次悄悄拿到旧响应,调用方会误以为新参数已经生效。若两个首请求同时穿过查重,又会产生两次真实写入。REST API 版本控制解决接口形状演进,MCP 超时、重试与幂等讨论工具调用的恢复策略;这里聚焦 HTTP 写接口本身的原子接受合同。

先固定操作身份和摘要规则

发布不可变的 http.idempotency.v1 合同。Idempotency-Key 的 rev-07 文档只是已过期 Internet-Draft,不是 RFC;本地合同才是实际规范。去重作用域至少包含认证后的 tenant、principal 或调用应用、路由的业务 operation、API major revision 与客户端键,不能让请求体自报 tenant,也不能跨用户共享命中。键必须是高熵、不含邮箱或订单内容的 opaque 值,并限制长度、字符集与每主体活跃数量。API 限流策略仍负责滥用流量;幂等表不是无限容量的限流器。

requestFingerprint 绑定会改变业务效果的全部输入:规范化路径参数、选定 query、媒体类型、业务 header 版本和 body。原始字节完全相同的协议可直接哈希字节;若合同允许语义等价 JSON,则先拒绝重复成员名、非有限数值和超出 schema 的字段,再按固定 schema revision 规范化。RFC 8785 JCS 的 number 序列化会把 10.0 与 10 归成同一表示;若金额 scale 等词法差异有业务含义,schema 必须改用定点整数加 scale、十进制字符串或 typed canonicalization,不能直接套 JCS number。数组和 null 也只按显式合同转换。

text
scope = H(tenant, principal, operation, apiRevision, idempotencyKey)
fingerprint = H(method, canonicalTarget, mediaType,
                schemaRevision, canonicalPayload)

同一 scope、相同 fingerprint 才是重放;同一 scope、不同 fingerprint 是 PAYLOAD_CONFLICT,必须返回稳定的冲突错误且不暴露首请求正文。服务端应回显所采用的协议版本与 operation ID,不能把客户端提供的键直接当业务资源 ID。

用唯一记录定义线性接受点

幂等记录状态为 RESERVED -> EXECUTING -> SUCCEEDED | FAILED_FINAL | UNKNOWN,另有不产生效果的 REJECTED。第一请求在数据库事务中插入唯一 scope,保存 fingerprint、合同版本、operation ID、ownerGeneration 与恢复 lease;唯一约束冲突后锁定并读取已有记录。worker 只能以 CAS 领取或接管同一 operation,lease 到期只转移恢复所有权,不授权创建第二个效果。乐观锁与悲观锁解释 CAS 与行锁,关键是“判断不存在”和“占位”不能分成两个无保护步骤。

仅在占位成功后执行业务事务。若效果与幂等表同库,业务写、终态和 transactional outbox 一起提交;恢复器先读该事务结果,未提交才由更高 owner generation 继续。响应快照只保存已批准的状态码、媒体类型、资源位置、业务版本和 body 或加密 body 摘要,排除 Date、trace ID、一次性 token 与连接相关 header。后续相同请求在 SUCCEEDED/FAILED_FINAL 返回同一业务结果;RESERVED/EXECUTING 返回处理中状态或查询链接,不能另起执行。

下游效果无法与本地记录同事务时,必须在发送前登记绑定 owner generation 的 effect attempt 与 provider handle 槽,再把稳定下游幂等键传到底。进程若在发送边界崩溃,状态保持 UNKNOWN;恢复器只能按 handle 查询、用原键重放或人工对账,不能换键,也不能因 lease 过期宣称效果未发生。后台任务队列可承载恢复,但至少一次投递不等于一次业务效果。

键的保留期必须覆盖客户端最大重试、队列积压、下游对账和备份恢复窗口。到期先留下 scope 与 fingerprint tombstone;若业务无法证明旧效果不可再出现,就拒绝复用该 scope,而不是删除记录后允许第二次扣款。取消只阻止尚未跨过接受点的请求;已提交操作要返回当前结果或进入明确补偿流程。

用竞争和丢响应验收

故障套件对同一键并发发送 100 个相同 payload,再混入一个不同 payload;在占位后、业务提交前、提交后回响应前和下游成功后分别杀进程;还要制造事务死锁、连接池超时、旧客户端跨 API revision 重放、认证主体改变、JSON 成员顺序变化、重复字段和 tombstone 到期竞争。断言相同请求只有一个 operation ID、业务行和 outbox effect,不同摘要全部冲突,UNKNOWN 未对账前不会升代执行。

上线门要求 10 万次并发重放中重复逻辑效果为 0、payload 冲突误命中为 0、终态重放响应摘要一致率 100%、UNKNOWN 超过恢复 SLO 的比例低于 0.01%,且 P99 占位事务不超过接口延迟预算的 5%。先 shadow,再按 operation 放量。回滚时先提升 writer epoch、停止新接受并排空或隔离未决 operation;新旧 writer 必须共用同一幂等 registry。旧实现若无法消费该 registry,只能退到只读/人工路径,不能恢复旧写入口让客户端重试绕过新表。新表与 outbox 始终保留供对账。

延伸阅读

Related

相关推荐