[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fc38d7HMfOFztLyxAsdtsLtXMavKDee1aDvV0RCvyrhQ":3},{"item":4,"related":44},{"id":5,"type":6,"title":7,"slug":8,"summary":9,"body":10,"coverUrl":11,"productScreenshots":12,"productLinks":13,"authorName":14,"authorUrl":15,"authorSubject":16,"category":17,"tags":22,"sourceLabel":35,"sourceName":36,"sourceUrl":36,"status":37,"seoTitle":36,"seoDescription":36,"canonicalUrl":36,"isFeatured":38,"sno":39,"sortOrder":40,"publishedAt":41,"updatedAt":42,"createdAt":43},"1bf18a10-9567-4451-ac3c-54eda63aa8bc","article","幂等键：支付按钮点两次，为什么不能扣两次钱","idempotency-key-payment-retry-explained","网络超时并不等于支付没有发生。Idempotency-Key 让客户端可以安全重试，服务端则通过唯一约束、请求指纹、状态机和结果复用把重复请求变成同一次业务操作。","支付按钮被用户点了两次，网络却只返回了一次结果；客户端以为请求失败，自动重试了一遍；服务端其实已经扣款成功，只是响应在路上丢了。如果接口把每次 POST 都当作一笔新操作，重试就可能变成重复扣款。\n\n幂等性解决的不是“请求永远只到达一次”，而是“同一业务意图被重复执行时，最终效果仍然可控”。HTTP 规范把 GET、PUT、DELETE 等方法的幂等语义和 POST 的非幂等特性区分开；针对 POST\u002FPATCH 的 `Idempotency-Key` 目前仍是 IETF Internet-Draft，而不是已经发布的 RFC。[HTTP Idempotency-Key 草案](https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fdraft-ietf-httpapi-idempotency-key-header\u002F)\n\n## 先区分三个容易混淆的概念\n\n### 至多一次\n\n客户端只发送一次，失败就不重试。重复执行风险低，但用户体验差，网络抖动时容易出现“其实成功了，但页面显示失败”。\n\n### 至少一次\n\n客户端或消息系统会重试，直到得到确认。成功率更高，但服务端必须能够识别重复意图，否则同一笔业务可能执行多次。\n\n### 幂等\n\n同一个业务操作执行一次和执行多次，最终状态一致。它不代表每次响应都完全相同，也不代表所有副作用天然安全；它要求系统为“重复请求”定义明确行为。\n\n## Idempotency-Key 是怎么工作的\n\n客户端为一次业务意图生成唯一键，并在重试时复用它：\n\n```http\nPOST \u002Fpayments\nIdempotency-Key: \"pay-8e03978e-40d5-43e8-bc93-6894a57f9324\"\nContent-Type: application\u002Fjson\n\n{\"orderId\":\"order-123\",\"amount\":1999,\"currency\":\"CNY\"}\n```\n\n服务端通常需要保存三类信息：\n\n- 幂等键属于哪个租户或用户。\n- 第一次请求的请求指纹，例如规范化后的请求体哈希。\n- 业务处理状态、结果和可重复返回的响应。\n\n第一次收到键时，服务端创建一条“处理中”记录，并在同一个事务边界内推进业务。后续收到相同键时，如果业务已经完成，就返回已保存结果；如果仍在处理，可以返回处理中或让客户端稍后重试；如果请求体与第一次不同，则必须拒绝，避免一个键被复用于两个业务意图。\n\n```mermaid\nflowchart TD\n    A[收到带 Idempotency-Key 的请求] --> B[查找键与请求指纹]\n    B -->|没有记录| C[创建处理中记录]\n    C --> D[执行业务事务]\n    D --> E[保存结果并标记完成]\n    B -->|已有完成记录| F[返回原结果]\n    B -->|已有处理中记录| G[返回处理中或稍后重试]\n    B -->|键相同但请求不同| H[拒绝复用该键]\n    E --> I[客户端收到成功响应]\n```\n\n## 幂等键不是简单加一列字符串\n\n### 原子占位很重要\n\n两个并发请求可能同时查不到键，如果只是“先查询，再插入”，两者都可能开始扣款。应使用数据库唯一约束、原子插入或带条件的状态更新，让只有一个请求获得处理权。\n\n### 业务状态和幂等状态要配合\n\n如果幂等记录写成功，但支付事务失败，重试应该允许恢复；如果支付成功但幂等记录没保存，重试仍可能再次调用支付渠道。真正可靠的设计需要把业务状态、外部副作用和幂等记录放进一致的流程里，必要时使用状态机、Outbox 或支付渠道提供的幂等能力。\n\n### 响应要不要保存\n\n保存完整响应最方便重试，但可能占用空间，也要考虑敏感信息。只保存业务结果和重新构造响应，可以降低存储成本，但要求响应构造本身稳定。错误响应是否缓存，也应按错误类型区分：参数错误可以固定返回，临时超时则可能需要再次执行。\n\n## 外部服务让问题更难\n\n你的数据库事务不能自动回滚银行、短信服务或物流平台的动作。于是“扣款成功但本地超时”成为必须建模的状态，而不是异常分支。\n\n常见做法包括：\n\n- 传递同一个幂等键到支持幂等的下游服务。\n- 为外部调用记录发送状态、响应和重试次数。\n- 用可查询的业务状态机区分待处理、成功、失败和未知。\n- 对未知状态先查询下游，不要直接再次扣款。\n- 把副作用事件写入可靠队列，使用 Outbox 避免数据库提交成功但消息丢失。\n\n## 幂等键的生命周期怎么定\n\n保存太短，客户端晚一点重试可能被当成新请求；保存太久，会增加存储成本，还可能让历史键与新的业务语义冲突。生命周期应该由业务最大重试窗口、对账周期和风险决定，而不是随便设置一个小时。\n\n幂等键还必须带租户或用户边界。全局只按字符串查找，可能导致两个不同租户恰好使用同一个键时互相影响。建议使用“租户 ID + 幂等键”作为唯一范围，并限制键长度、格式和请求体大小。\n\n## 它和去重、分布式锁不完全一样\n\n- 去重关注“这条事件是否处理过”，幂等性关注“重复执行这次业务意图时如何得到一致结果”。\n- 分布式锁关注同一时刻谁能进入临界区，幂等记录关注之后的重试如何复用结果。\n- 数据库唯一索引可以阻止重复资源，但不一定能返回第一次请求的完整响应。\n\n它们经常一起使用，但不能互相替代。一个锁释放后，第二次请求仍然可能执行；一个唯一键报错后，客户端仍然需要知道第一次操作到底成功还是失败。\n\n## 一句话带走\n\n网络请求永远可能超时、重复或丢响应。幂等键把“重复执行”从不可预测的副作用，变成可查询、可恢复、可审计的业务状态。支付、订单、发票和资源创建接口，应该在写第一行代码时就决定重试语义，而不是等第一次重复扣款后再补救。\n\n## 延伸阅读\n\n- [RFC 9110：HTTP Semantics](https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc9110.html)\n- [IETF Idempotency-Key Internet-Draft](https:\u002F\u002Fdatatracker.ietf.org\u002Fdoc\u002Fdraft-ietf-httpapi-idempotency-key-header\u002F)\n- [Web API 请求示意图](https:\u002F\u002Fcommons.wikimedia.org\u002Fwiki\u002FFile:Web_API_diagram.svg)","\u002Fuploads\u002F2026-08-09\u002F605338be-1d8a-401d-9e4c-f02a7b3d26b6.jpg",[],[],"Foundit","https:\u002F\u002Ffoundit.cn","f39339b1-aaa6-4e86-b0c2-a6e6a21113b5",{"id":18,"name":19,"slug":20,"description":21},"6179d3b6-dc34-4483-9ded-3cd9f1b37a47","科普","abbreviation","介绍各领域新兴概念",[23,27,31],{"id":24,"name":25,"slug":26},"7c76bfc2-f80f-4ee0-a95d-27bd8708b434","技术","slug",{"id":28,"name":29,"slug":30},"4c2bbea6-eab7-40a8-8447-1de478ff7749","分析","analyse",{"id":32,"name":33,"slug":34},"144abe77-0dc6-4f66-a176-20bddb1c0bfa","编程","coding","资料来源",null,"published",false,66,0,"2026-08-08T00:00:00.000Z","2026-08-09T12:49:52.200Z","2026-08-09T12:13:33.386Z",[45,54,63],{"id":46,"type":6,"title":47,"slug":48,"summary":49,"coverUrl":50,"authorName":14,"sno":51,"publishedAt":52,"createdAt":53},"62724dbd-ce49-4be9-afa9-4ba127513a62","语音搜索一定先变成文字吗？AI 开始绕过转写这一步","speech-to-retrieval-without-transcription","传统语音搜索先把声音转成文字，再拿文字查资料，一次听错就可能让搜索方向完全跑偏。Speech-to-Retrieval 尝试直接把语音与相关文档映射到同一个语义空间。本文用蒙克名画的例子讲清新旧架构及其边界。","\u002Fuploads\u002F2026-09-08\u002Fe1173842-1ec4-48f3-8349-240fc2d78197.jpg",50,"2026-08-30T00:00:00.000Z","2026-08-14T03:06:11.525Z",{"id":55,"type":6,"title":56,"slug":57,"summary":58,"coverUrl":59,"authorName":14,"sno":60,"publishedAt":61,"createdAt":62},"0999bb14-a97c-4003-84e7-61ad2da998e0","文件删除以后去了哪里？为什么有时还能恢复？","where-deleted-files-go-data-recovery","普通删除往往只是移除文件系统里的索引并把空间标记为可重用，数据本身未必立即消失；SSD 的 TRIM、磨损均衡与加密又让情况更加复杂。本文区分回收站、删除、覆盖和安全清除，说明何时应停止写入设备。","\u002Fuploads\u002F2026-09-08\u002Fb7222764-de75-4863-8679-0a94a8965af3.jpg",51,"2026-08-20T00:00:00.000Z","2026-08-14T03:06:15.710Z",{"id":64,"type":6,"title":65,"slug":66,"summary":67,"coverUrl":68,"authorName":14,"sno":69,"publishedAt":70,"createdAt":71},"55f42805-a25a-4ce3-89d9-121b9a268a8f","0.1＋0.2 为什么不等于 0.3？计算机真的算错了吗？","why-point-one-plus-point-two-not-point-three","许多十进制小数无法用有限位二进制精确表示，计算机只能保存最接近的值。微小误差在显示时常被隐藏，却会在比较、累计和金额计算中冒出来。本文用三分之一的类比讲清浮点数，并给出可靠的处理原则。","\u002Fuploads\u002F2026-09-08\u002Ffd3a7daf-2a2c-4ef0-8f57-38f1d9de962c.jpg",54,"2026-09-06T00:00:00.000Z","2026-08-14T03:06:15.129Z"]