HTTP 重试与幂等键:超时之后怎样避免重复创建任务

10-01 5阅读

客户端提交创建任务请求后超时,服务器可能什么都没做,也可能已经创建成功,只是响应丢了。直接重发可能生成两项任务,完全不重试又可能让用户以为操作失败。幂等键的作用是把“同一项业务意图的再次尝试”关联起来。本文讨论自建接口设计,HTTP 语义以 RFC 9110 为依据,数据库示例采用 PostgreSQL 18。

HTTP 重试与幂等键:超时之后怎样避免重复创建任务

AI生成概念配图:重复请求携带同一操作标识,关联到同一份结果。仅辅助理解,不代表真实界面或实测结果。

先区分方法语义与接口契约

HTTP 对幂等方法的定义关注重复相同请求的预期效果,并不要求每次响应字节完全相同。POST 本身不自动拥有这个保证。某些 API 提供 Idempotency-Key 等机制,但是否支持、作用域、保留周期和错误重放规则,都应以目标 API 文档为准;随便添加一个同名请求头不会让服务器自动去重。

例如 Stripe 明确描述了其幂等请求结果保存与参数检查行为,这可以作为阅读接口契约的例子,却不能直接推断其他平台采取同样规则。接入前应先回答:哪些请求能用、哪些错误可以重试、同键并发会怎样、结果保留多久,以及键过期后再次提交意味着什么。

一项业务意图只生成一个稳定键

客户端应在首次提交前生成足够随机的键,并与该项待完成操作一起保存。连接超时后的重试继续使用原键和相同业务参数;用户确实发起另一项任务时再生成新键。若每次点击重试都换键,服务端无法知道它们属于同一次操作。键本身也不应包含姓名、联系方式或其他不必要的个人信息。

服务端可以把键的作用域限定为租户、操作类型与键值的组合,同时记录业务参数的稳定摘要。相同键却携带不同参数时,应明确拒绝,避免把另一个任务的结果误当成本次响应。摘要的计算需要先定义字段、类型与规范化规则,不要仅因 JSON 字段顺序不同,就把同一业务意图误判成不同参数。

用存储约束解决并发竞争

CREATE TABLE request_dedup (
  tenant_id bigint NOT NULL,
  operation text NOT NULL,
  request_key text NOT NULL,
  payload_hash text NOT NULL,
  state text NOT NULL,
  result_id bigint,
  created_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (tenant_id, operation, request_key)
);

这只是用于解释唯一约束的表结构,不是可直接上线的完整实现。两个并发请求都可能在“先查询是否存在”时得到不存在,所以必须依赖数据库唯一性和事务处理竞争。若任务创建与去重记录都在同一数据库,应设计为同一事务中的一致状态;提交成功但响应丢失时,后续请求才能找到既有结果。

记录处理中状态后,还需要定义崩溃恢复、租约或超时处理,避免永远卡在进行中。重复请求遇到处理中状态时,是等待、返回查询地址还是返回可重试错误,应写成稳定契约。保存和重放结果时仍需检查当前调用者权限,不能让知道键值的人读取其他用户的结果。

重试策略与副作用边界要一起设计

客户端只对接口约定的可重试场景重试,并设置等待退避、随机抖动、总时限和次数上限。不要在鉴权失败或参数错误时无限重发;超时后也不要立即换键“再试一次”。若服务已经提供任务状态查询入口,查询既有操作通常比创建另一项操作更容易解释。

数据库事务不能自动覆盖外部邮件、第三方任务或其他系统。此时需要可靠事件投递、下游去重和补偿流程共同配合,不能宣称有一张去重表就实现全链路只执行一次。键的保留期到达后,旧键再次出现也必须按文档处理;过早清理可能重新引入重复操作。

验收至少覆盖响应丢失、同键并发、同键不同参数、服务崩溃和保留期边界。幂等性真正保护的是业务效果,测试也应检查任务数量与状态,而不只是观察是否返回了相同状态码。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。