当前位置: 首页 > news >正文

幂等性难题:第二次请求不同时如何应对?

幂等性:直到第二次请求不同时才变得复杂

2026 年 5 月 7 日,阅读时长 25 分钟。涉及标签有 [api]、[http]、[idempotency]、[backend]、[distributed - systems]、[databases]、[microservices]、[architecture]、[payments]。人们常认为幂等性问题已解决,方法是在请求中添加 `Idempotency - Key`,存储响应,重试时重放该响应。理想情况下,实现简单。客户端发送请求示例如下:

POST /payments Idempotency - Key: abc - 123 Content - Type: application/json { "accountId": "acc_1", "amount": "10.00", "currency": "EUR", "merchantReference": "invoice - 7781" }

服务器会检查是否处理过 `abc - 123` 键,未处理则创建支付记录,已处理则返回之前响应。但真正挑战从第二次请求开始,因为第二次请求并非总是第一次的简单重放。

第二次请求可能是完整重放,直接返回存储结果;可能在第一次请求处理时到达,幂等层成并发控制一部分;可能第一次请求创建本地支付记录后崩溃,导致本地记录和外部副作用不同步;可能第一次请求调用支付提供商,进程挂掉后数据库无法推断资金是否转移;还可能第二次请求用相同键但内容不同,如:

{ "accountId": "acc_1", "amount": "100.00", "currency": "EUR", "merchantReference": "invoice - 7781" }

这种情况让幂等性变得复杂,服务器需明确策略,对于有副作用的 API,使用相同作用域键但规范命令不同时,应视为严重错误。以下情况重放缓存无法解释:完整重放、并发重试、部分本地成功、下游未知状态、相同键但规范命令不同、无键重复操作、过期后重试、部署等变更后重试。若设计只处理完整相同命令重试,只是重放缓存,不能解决所有问题。

幂等性关乎效果

若一个操作执行一次或多次预期效果相同,该操作就是幂等的。关键在于“效果”一词。HTTP 提供方法级语义,如 `PUT /users/123/email` 重复发送相同表示能使资源保持相同状态,就是幂等的;`DELETE /sessions/456` 删除已删除会话仍意味着“会话不存在”,也是幂等的,重复执行 `DELETE` 可能返回 `404`,但效果幂等。然而,处理程序仍可能产生业务关心的重复副作用。`POST` 请求通常默认不幂等,但服务器存储并强制执行正确行为,也可使其幂等。键标识操作,但不能定义请求等价性等。唯一性约束可防止一类重复,但不能为客户端提供正确重试结果。

你需要记住的要点

对于 `POST /payments` 请求,持久的幂等记录需回答三个问题:谁拥有这个键?第一个命令的含义是什么?可以重放的结果是什么?用类似 PostgreSQL 的 SQL 语言,最小的表可能如下:

create table idempotency_requests ( tenant_id text not null, operation_name text not null, idempotency_key text not null, request_hash text not null, status text not null, response_status int, response_body jsonb, resource_type text, resource_id text, error_code text, created_at timestamptz not null, updated_at timestamptz not null, expires_at timestamptz not null, locked_until timestamptz, primary key (tenant_id, operation_name, idempotency_key) );

键通常不是全局唯一,作用域可多种选择,操作名称防止键意外重用,`request_hash` 是服务器对第一个命令的记忆。`IN_PROGRESS` 不是内部细节,行为需明确,如下表:

现有记录规范命令是否相同?建议行为
插入 `IN_PROGRESS` 并执行
`COMPLETED`重放存储的响应或文档规定的等效响应
任何现有记录以幂等冲突拒绝
`IN_PROGRESS`,新记录等待,返回 `202`,或返回 `409` + `Retry - After`
`IN_PROGRESS`,旧记录恢复所有权;不要盲目再次执行
`FAILED_REPLAYABLE`重放存储的失败响应
`FAILED_RETRYABLE`根据策略允许重试
`UNKNOWN_REQUIRES_RECOVERY`触发协调或返回待处理/恢复状态
过期/已删除未知遵循文档规定的过期行为

响应字段存在是因为幂等性不仅防止重复写入,客户端需要答案。存储完整响应体或存储资源引用各有优缺点。

相同键,不同命令

这是幂等层应明确捕捉的错误。示例如下:

第一个请求:

{ "accountId": "acc_1", "amount": "10.00", "currency": "EUR", "merchantReference": "invoice - 7781" }

第二个请求:

{ "accountId": "acc_1", "amount": "100.00", "currency": "EUR", "merchantReference": "invoice - 7781" }

相同 `Idempotency - Key: abc - 123`,不同金额。返回原始响应会掩盖客户端错误,对于有副作用的 API,应视为严重错误,返回 `409 Conflict` 等。常见客户端错误示例及更好做法如下:

错误示例: idempotencyKey = cartId POST /payments amount = 10.00 key = cart_123 POST /payments amount = 15.00 key = cart_123 更好的做法: idempotencyKey = paymentAttemptId

服务器不应猜测购物车键代表的支付。也可设计 `(key + content hash)` 定义操作标识,但要让客户端清楚。

对命令进行哈希,而不是对字节进行哈希

对于 JSON API,原始字节比较通常过严,如以下两个请求体通常应等效:

{ "amount": "10.00", "currency": "EUR" } { "currency": "EUR", "amount": "10.00" }

默认值和未知字段情况需在哈希前决定。实际规则是对经过验证的命令进行哈希,合理流程如下:

  1. 将请求解析为版本化的请求 DTO 或命令。
  2. 对 API 视为等效的值进行规范化。
  3. 排除仅用于传输的元数据。
  4. 包含路径参数和操作名称。
  5. 若语义头影响操作,将其包含在内。
  6. 确定影响响应形状的头属于命令哈希、重放契约还是两者都不属于。
  7. 排除 `Authorization` 和幂等键本身。
  8. 进行规范序列化。
  9. 使用稳定的算法进行哈希。

支付示例指纹可能包括:

operation: create_payment accountId: acc_1 amount: 10.00 currency: EUR merchantReference: invoice - 7781 channel: web apiVersion: 2026 - 05 - 01

要注意金额等因素,请求哈希是契约,改变计算方式旧重试可能不同。

首次插入决定谁拥有执行权

两个相同请求几乎同时到达两个 API 实例:

POST /payments Idempotency - Key: abc - 123

以下实现有问题:

existing = find_by_key(key) if existing does not exist: create_payment() insert_idempotency_record()

正确插入方式如下:

insert into idempotency_requests (tenant_id, operation_name, idempotency_key, request_hash, status, created_at, updated_at, expires_at, locked_until) values (:tenant_id, 'create_payment', :idempotency_key, :request_hash, 'IN_PROGRESS', now(), now(), now() + interval '24 hours', now() + interval '30 seconds') on conflict do nothing;

然后根据不同情况处理:

if rows_inserted == 1: this request owns execution else: existing = load idempotency row if existing.request_hash != request_hash: return 409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_REQUEST if existing.status == COMPLETED: return replay(existing.response_status, existing.response_body) if existing.status == IN_PROGRESS and existing.locked_until > now(): return 202 or 409 + Retry - After if existing.status == IN_PROGRESS and existing.locked_until <= now(): attempt recovery ownership # this must be atomic too if existing.status == UNKNOWN_REQUIRES_RECOVERY: trigger reconciliation or return pending/recovery response

恢复所有权必须原子操作。在简单本地情况,所有者可在事务中完成幂等记录和支付记录等。外部副作用会改变情况,Redis 的 `SET NX EX` 只是执行保护,不能替代对操作结果的记忆。

提供商超时意味着你的保证结束

重要失败情况如下:

  1. API 接收到 `POST /payments` 请求。
  2. 插入幂等记录,状态为 `IN_PROGRESS`。
  3. 创建本地支付记录 `pay_789`。
  4. 调用下游支付提供商。
  5. 提供商接收请求并处理成功。
  6. API 超时、崩溃或丢失提供商响应。
  7. 客户端使用相同的键重试。

本地状态机可能如下:

RECEIVED LOCAL_PAYMENT_CREATED PROVIDER_REQUEST_SENT PROVIDER_CONFIRMED COMPLETED UNKNOWN_REQUIRES_RECOVERY

重试行为取决于状态。若提供商无幂等键和查询 API,系统存在操作漏洞。下游调用需稳定标识。除非 API 有特定原因,应避免使用 `425 Too Early`。

重放是一种契约,而非便利

对于已完成的幂等请求,重放相同状态和响应体最不易混淆,如:

HTTP/1.1 201 Created Idempotent - Replayed: true Content - Type: application/json { "paymentId": "pay_789", "status": "PENDING", "accountId": "acc_1", "amount": "10.00", "currency": "EUR", "merchantReference": "invoice - 7781" }

从当前资源状态重新构建响应可能有问题,架构变更会使情况更糟。常见折衷方案是存储相关信息,只对需要精确重放的端点存储完整响应体。

队列消费者也有同样的问题

HTTP 请求受关注多,但很多重复副作用发生在消费者等中。假设支付服务发布事件,消费者接收到两次不应产生重复副作用。去重键可多种选择,消费者收件箱表可能如下:

consumer_inbox - consumer_name - message_id - status - processed_at - error_code unique(consumer_name, message_id)

标记消息已处理不简单,通常做法是在发送副作用前持久化。分类账条目有自然幂等键。从消费者角度,生产队列集成多是至少一次交付,精确一次交付不意味着精确一次业务效果。外发/内联模式如下:

在同一个数据库事务中: insert payment row pay_789 insert outbox event PaymentCreated(pay_789) 发布者: 读取未发布的外发事件 发布带有 eventId 的事件 标记外发事件已发布 消费者: 根据 eventId 或业务操作键去重 在唯一约束后写入副作用

幂等性可防止一些重复,但不能消除有害消息等问题。

过期是 API 契约的一部分

幂等记录通常不能永久存在,重放窗口是产品/API 决策。已完成记录过期后可删除响应体保留元数据。旧的 `IN_PROGRESS` 状态需单独处理,清理作业不应盲目删除正在进行中的记录。重放次数用于容量规划,相关指标可发现问题。

失败重放是一个策略决策

不能将每个失败都视为“可以安全重试”或“已完成”。纯粹语法验证失败通常无需幂等存储,业务拒绝需决定第一个决策是否对幂等键有约束力。确定性拒绝可能可重放,但账户余额变化时可能不适用。身份验证失败不应创建幂等记录,授权失败需谨慎。速率限制通常不应记录为已完成幂等结果。产生副作用前的服务器错误通常可重试,之后的错误危险。实用内部状态集如下:

IN_PROGRESS COMPLETED FAILED_REPLAYABLE FAILED_RETRYABLE UNKNOWN_REQUIRES_RECOVERY EXPIRED

不要直接暴露每个内部状态,将失败简单分类会使恢复困难。

当一个事务无法涵盖操作时

有用区别是一个持久事务能否涵盖操作。若可以,本地部分简单;当副作用跨越边界,每个边界需有重复抑制规则。更好模型是维护稳定操作标识。主动 - 主动多区域部署中,区域本地幂等表有局限。高吞吐量 API 中,幂等表可能成热点路径,需分区处理。

何时不构建通用幂等层

成本在于持久记忆和恢复行为。对于重复操作无害且易发现的管理操作、只读操作、重复分析事件成本低且可下游纠正的情况,无需构建支付级幂等层。某些操作使用业务键更好,可改变资源模型。客户端能识别重试时,客户端生成的键有用。根据重复副作用危害等决定机制。

值得测试的失败模式

以下测试比理想情况单元测试更有价值:

相同键,相同规范命令,已完成

第一个请求创建支付,返回 `201 Created`,`paymentId = pay_789`。第二个相同请求返回相同存储结果,不创建新支付和发布新事件。

相同键,不同规范命令

两个请求键相同但金额不同,预期以稳定机器可读幂等冲突拒绝请求并记录统计。

两个并发的相同请求

同时启动两个相同键和命令的请求,一个请求获执行权,另一个等待或返回稍后重试响应,副作用只执行一次。

下游成功后超时

模拟提供商成功后客户端崩溃,重试请求不应使用新操作标识调用提供商,应找到本地状态或进入恢复流程。

队列中的重复消息

两次传递 `PaymentCreated(pay_789)` 事件,只创建一条分类账条目等,重试完成未完成工作。

过期或陈旧状态

在幂等记录过期、处于陈旧 `IN_PROGRESS` 状态、响应架构更改后或从另一个区域重试,这些是网络重试常见边界情况。

发布前的检查清单

  • 拒绝使用相同作用域键但规范命令不同的请求。
  • 对作用域键使用唯一约束或原子插入。
  • 对经过验证的命令进行哈希,而不是对原始 JSON 字节进行哈希。
http://www.jsqmd.com/news/799312/

相关文章:

  • 003-VXLAN集中式网关实验(命令详解版)
  • 告别Qt Creator的坑!用VS2017社区版+Qt5.14搭建C++ GUI开发环境(附完整避坑清单)
  • 从‘信不信由你’到‘算给你看’:聊聊主观贝叶斯在推荐系统和风控里的那些实战坑
  • 别再手动连线了!用Gephi导入Cora论文数据集,5分钟搞定网络图可视化
  • 别只算训练和推理成本:AI 评测正在变成新的算力账单,先把这 4 层预算拆开
  • 苹果手机玩不了安卓游戏?2026年云手机已经把这堵墙拆了
  • 告别编译噩梦:在Ubuntu 22.04上为你的C++项目搞定Abseil依赖的三种方法
  • OpenClaw技能安装器:自动化任务框架的模块化扩展核心
  • 上网行为怎么监控?教你五个简单实用的上网行为监控方法,建议收藏
  • 别再让QLabel文字显示不全了!手把手教你用QFontMetrics实现智能省略(附完整代码)
  • 告别码率尖峰:帧内刷新如何重塑视频传输的平稳性
  • 如何将B站缓存视频转为MP4:简单快速的m4s转换完整指南
  • Qt 委托模式实战:QItemDelegate 赋能 QTableView 单元格交互控件
  • 哪些论文排版网站能直接导出符合国标(GB/T 7714)的格式?
  • docker 运行xray
  • 免费开源AI软件.桌面单机版,可移动的AI知识库,察元 AI桌面版:本地离线知识库的真完全离线 内网无外网装察元AI的拼装步骤
  • 嵌入式系统调试技术:从JTAG到多核同步的实战指南
  • 打破 IT 业务壁垒:基于JiuwenClaw AgentTeam多智能体驱动电商数据飞轮实践,赋能电商数字化转型定义新范式
  • 利用MCP协议与AI实时追踪TikTok趋势,提升内容策略效率
  • 揭秘Java世界中oop-klass模型奥秘之C++眼中的Java类
  • Obsidian代码块美化终极指南:如何让技术笔记瞬间提升专业度
  • 保姆级教程:在Google Colab上用TensorFlow 2.0快速搭建你的第一个ACGAN图像生成器
  • 一名编程小白的从零开始
  • Grok 4.1 Fast 技术深度解析:架构、训练、能力与工程优化
  • 微服务配置管理新思路:轻量级配置中心管理器ccmanager实战解析
  • PowerShell玩转Excel COM对象:从入门到解决‘被呼叫方拒绝’报错
  • 第一篇:只是想说清楚每行代码是由谁执行的,怎样执行的
  • 结构化技能文档实践指南:从规范到团队知识库构建
  • 告别Jira和Trello?我用ONES的Wiki和测试模块重构了团队协作流程
  • 无线IoT系统硬件级时间同步方案设计与优化