<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>幂等性 on 止语Lab</title>
        <link>https://www.wujiachen.com.cn/tags/%E5%B9%82%E7%AD%89%E6%80%A7/</link>
        <description>Recent content in 幂等性 on 止语Lab</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Fri, 14 Aug 2026 16:33:34 +0800</lastBuildDate><atom:link href="https://www.wujiachen.com.cn/tags/%E5%B9%82%E7%AD%89%E6%80%A7/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>幂等性设计：唯一ID、状态机与乐观锁的三层防线</title>
            <link>https://www.wujiachen.com.cn/posts/idempotency-design/</link>
            <pubDate>Fri, 14 Aug 2026 16:33:31 +0800</pubDate>
            <guid>https://www.wujiachen.com.cn/posts/idempotency-design/</guid>
            <description>&lt;img src=&#34;https://img.wujiachen.com.cn/idempotency-design/cover.png&#34; alt=&#34;Featured image of post 幂等性设计：唯一ID、状态机与乐观锁的三层防线&#34; /&gt;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/idempotency-design/cover.png&#34; alt=&#34;封面&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;&lt;strong&gt;导读&lt;/strong&gt;：用户点了一次支付，扣了两次款。这是幂等性缺失。本文拆解三层防线——唯一ID、状态机、乐观锁，各防一类故障，以及层间配合的坑。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;做支付模块联调那阵子，一次压测里前端超时重试，同一个下单请求连着打进来三遍。翻日志的时候我盯着订单表愣了两秒——三笔订单，金额一模一样，时间戳间隔都在几十毫秒内。&lt;/p&gt;&#xA;&lt;p&gt;这是幂等性问题：同一个操作被执行了多次，产生了多个副作用。支付、下单、退款、转账，任何一个写操作在分布式系统里都可能被重试，如果系统不防重复，轻则数据脏，重则用户的钱被扣两次。&lt;/p&gt;&#xA;&lt;p&gt;市面上的文章讲幂等，多数只讲一个技巧：加个幂等键、建个状态机、或者用乐观锁。但真到了生产环境，你往往发现单靠一层根本挡不住。这篇文章把幂等拆成三层防线，每层防一类不同的故障。三层防线配齐，再加上层间配合与对账兜底，才能说你的写操作是幂等的。&lt;/p&gt;&#xA;&lt;h2 id=&#34;1-一笔订单的三次事故&#34;&gt;&lt;a href=&#34;#1-%e4%b8%80%e7%ac%94%e8%ae%a2%e5%8d%95%e7%9a%84%e4%b8%89%e6%ac%a1%e4%ba%8b%e6%95%85&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;1. 一笔订单的三次事故&#xA;&lt;/h2&gt;&lt;p&gt;先看一个贯穿全文的例子。一笔支付订单，从创建到完成，会经历三类故障。&lt;/p&gt;&#xA;&lt;h3 id=&#34;11-事故一重复提交&#34;&gt;&lt;a href=&#34;#11-%e4%ba%8b%e6%95%85%e4%b8%80%e9%87%8d%e5%a4%8d%e6%8f%90%e4%ba%a4&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;1.1 事故一：重复提交&#xA;&lt;/h3&gt;&lt;p&gt;用户下单，前端提交创建订单请求。网络超时，用户以为没成功，又点了一次。两次请求内容完全一样——就是开场那笔 998 的订单，订单表里躺着两笔，用户被扣了两次款。&lt;/p&gt;&#xA;&lt;p&gt;这是重复问题：同一个请求到达了多次。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/idempotency-design/ch1-duplicate-bill.png&#34; alt=&#34;用户重复点击支付导致重复扣款&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;12-事故二乱序回调&#34;&gt;&lt;a href=&#34;#12-%e4%ba%8b%e6%95%85%e4%ba%8c%e4%b9%b1%e5%ba%8f%e5%9b%9e%e8%b0%83&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;1.2 事故二：乱序回调&#xA;&lt;/h3&gt;&lt;p&gt;订单创建后，支付渠道会回调通知支付结果。支付成功回调先发出，但由于网络原因晚到了；发货系统的回调反而先到（回调来自内部发货系统，不是支付渠道）。如果系统直接按回调内容改状态，订单会先变成&amp;quot;已发货&amp;quot;，再被支付回调改回&amp;quot;已支付&amp;quot;。状态来回跳，中间出现&amp;quot;未支付却已发货&amp;quot;的错误状态。&lt;/p&gt;&#xA;&lt;p&gt;这是乱序问题：请求到达的顺序和业务期望的顺序不一致。&lt;/p&gt;&#xA;&lt;h3 id=&#34;13-事故三并发改单&#34;&gt;&lt;a href=&#34;#13-%e4%ba%8b%e6%95%85%e4%b8%89%e5%b9%b6%e5%8f%91%e6%94%b9%e5%8d%95&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;1.3 事故三：并发改单&#xA;&lt;/h3&gt;&lt;p&gt;运营和用户同时操作同一笔订单。运营要改金额，用户要取消。两个请求同时读到订单当前状态，各自计算，然后各自写回。后写的覆盖先写的，其中一个操作静默丢失。&lt;/p&gt;&#xA;&lt;p&gt;并发：多个请求同时修改同一份数据，后写覆盖先写。&lt;/p&gt;&#xA;&lt;h3 id=&#34;14-三层防线总览&#34;&gt;&lt;a href=&#34;#14-%e4%b8%89%e5%b1%82%e9%98%b2%e7%ba%bf%e6%80%bb%e8%a7%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;1.4 三层防线总览&#xA;&lt;/h3&gt;&lt;p&gt;三类故障，三个本质不同的成因，需要三层不同的防线：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;防线&lt;/th&gt;&#xA;          &lt;th&gt;防什么&lt;/th&gt;&#xA;          &lt;th&gt;核心机制&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;唯一ID&lt;/td&gt;&#xA;          &lt;td&gt;重复&lt;/td&gt;&#xA;          &lt;td&gt;每个请求带唯一标识，服务端只处理一次&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;状态机&lt;/td&gt;&#xA;          &lt;td&gt;乱序&lt;/td&gt;&#xA;          &lt;td&gt;状态只能按合法路径迁移，非法迁移拒绝&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;乐观锁&lt;/td&gt;&#xA;          &lt;td&gt;并发&lt;/td&gt;&#xA;          &lt;td&gt;带版本号的更新，版本不符则失败&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/idempotency-design/ch1-three-defenses.png&#34; alt=&#34;三层防线总览——三道闸门各防一类故障&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;三层分工清晰：重复、乱序、并发，各管一类故障。你只上第一层，防住了重复，但防不住乱序和并发。这也是为什么说幂等是一个体系。&lt;/p&gt;&#xA;&lt;p&gt;澄清一个常见混淆：幂等和事务不是一回事。事务保证&amp;quot;要么全做要么全不做&amp;quot;（ACID 的原子性），解决的是&amp;quot;执行到一半失败&amp;quot;的问题；幂等保证&amp;quot;做多次和做一次效果相同&amp;quot;，解决的是&amp;quot;同一个操作被重复执行&amp;quot;的问题。两者互补——一个写操作应该既在事务里（原子性），又带幂等键（可重试）。单靠事务的 rollback 无法解决重复请求：两个请求各自开事务、各自成功提交，结果就是两份数据，事务并不知道这两个请求是同一个操作。另外按 HTTP 语义（RFC 9110），POST 天然不幂等——幂等键正是给 POST 类操作补上幂等语义；GET/PUT/DELETE 则天然幂等，不需要幂等键。&lt;/p&gt;&#xA;&lt;h2 id=&#34;2-第一层唯一id-防重复&#34;&gt;&lt;a href=&#34;#2-%e7%ac%ac%e4%b8%80%e5%b1%82%e5%94%af%e4%b8%80id-%e9%98%b2%e9%87%8d%e5%a4%8d&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2. 第一层：唯一ID 防重复&#xA;&lt;/h2&gt;&lt;h3 id=&#34;21-什么是唯一id&#34;&gt;&lt;a href=&#34;#21-%e4%bb%80%e4%b9%88%e6%98%af%e5%94%af%e4%b8%80id&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2.1 什么是唯一ID&#xA;&lt;/h3&gt;&lt;p&gt;唯一ID（也叫幂等键、Idempotency Key）是客户端在每次请求时生成的一个全局唯一标识。服务端看到同一个唯一ID，就知道这是同一个请求的重试，而不是新请求。Google Cloud 也采用类似机制：Cloud Tasks 对每个任务分配唯一 name，重复投递由服务端去重。&lt;/p&gt;&#xA;&lt;p&gt;它的经典实现是 Stripe 的 Idempotency-Key 模式：客户端生成一个 UUID 放进请求头。服务端首次收到时执行业务并保存结果（状态码 + 响应体）；后续收到相同 key 的请求，直接返回保存的结果，不再执行。&lt;/p&gt;&#xA;&lt;p&gt;唯一ID 的生成有两种选择：客户端生成和服务端生成。客户端生成（Stripe 模式）零额外请求，天然支持重试——客户端重发时带上同一个 key 即可。风险是客户端可能伪造或误用 key（拿 A 的 key 发 B），所以服务端要校验 key + 请求内容的一致性（Stripe 保存首次请求参数哈希，后续参数不同直接报错）。服务端生成（下发 token 模式）可控性更好，key 由服务端签发，客户端只负责透传；代价是创建订单前多一次&amp;quot;获取 token&amp;quot;的调用，且 token 过期后客户端没法重试同一个操作。支付类场景我倾向客户端生成 + 参数校验：重试能力是幂等的核心价值。&lt;/p&gt;&#xA;&lt;h3 id=&#34;22-唯一id-怎么做&#34;&gt;&lt;a href=&#34;#22-%e5%94%af%e4%b8%80id-%e6%80%8e%e4%b9%88%e5%81%9a&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2.2 唯一ID 怎么做&#xA;&lt;/h3&gt;&lt;p&gt;服务端需要一张幂等表，key 是唯一ID，加唯一约束：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;idempotency&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;idempotency_key&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;VARCHAR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;255&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;PRIMARY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;KEY&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;request_hash&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;   &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;VARCHAR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;64&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;response_body&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;TEXT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;         &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;     &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TIMESTAMP&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DEFAULT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;NOW&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;处理逻辑：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;收到请求 → 查幂等表（按 key）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  ├─ 存在 → 返回首次结果（不再执行）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  └─ 不存在 → 执行业务 → 写入幂等表（无论成败都记录首次结果；key 唯一约束保证并发只成功一个）→ 返回结果&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;关键点：唯一约束是并发安全的保证。两个重复请求同时到达，都查不到记录，都执行业务，但写表时唯一约束会让一个成功一个失败——失败的要么返回成功者的结果，要么重试查表。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/idempotency-design/ch2-idempotency-key.png&#34; alt=&#34;幂等键处理流程——登记处盖章去重&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;失败也写入（记录失败结果），重试返回同样的失败，避免重试重新执行业务产生部分副作用。这一层把&amp;quot;重复执行&amp;quot;拦在了幂等表 + 数据库唯一约束的双保险上。&lt;/p&gt;&#xA;&lt;h3 id=&#34;23-实测数据&#34;&gt;&lt;a href=&#34;#23-%e5%ae%9e%e6%b5%8b%e6%95%b0%e6%8d%ae&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2.3 实测数据&#xA;&lt;/h3&gt;&lt;p&gt;我跑了一组 Go 实验验证这个机制（[实测 Go 1.26.2]）：模拟同一请求并发重发 100 次，对照组无幂等键，实验组有幂等键表（唯一约束 + 首次结果缓存）。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;对照组A（无幂等键）: 重发 100 次 → 订单数 100（重复入账 99 笔）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;实验组B（有幂等键）: 重发 100 次 → 订单数 1（重复入账 0 笔）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;第 101 次重试同 key 同参数: 返回订单 order-1, 新建=false, 状态=REPLAY&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;同 key 不同金额: PARAM_MISMATCH: 同 key 不同参数，服务端拒绝&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;结论很直接：&lt;strong&gt;无幂等键，重发 100 次产生 100 个订单；有幂等键，100 次只有 1 个&lt;/strong&gt;。另外注意最后一行——同 key 不同参数会被拒绝，这是 Stripe 也强调的防误用机制：key 必须绑定请求内容，防止客户端拿一个 key 发不同请求。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/idempotency-design/ch2-e1-benchmark.png&#34; alt=&#34;E1 实测数据——100 笔 vs 1 笔&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;24-幂等键的存储与过期&#34;&gt;&lt;a href=&#34;#24-%e5%b9%82%e7%ad%89%e9%94%ae%e7%9a%84%e5%ad%98%e5%82%a8%e4%b8%8e%e8%bf%87%e6%9c%9f&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2.4 幂等键的存储与过期&#xA;&lt;/h3&gt;&lt;p&gt;幂等表放哪？两个选择：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;数据库&lt;/strong&gt;（强一致）：金融级场景，可靠但稍慢；幂等表在高峰期是高频读写热表，需规划清理任务（定时批删或惰性删除）并监控表膨胀&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Redis&lt;/strong&gt;（高速缓存）：性能好，但有过期窗口&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Stripe 的官方做法是 key 至少保留 24 小时，之后可移除。窗口怎么定？看你的业务重试窗口——我认为幂等键的保留时间必须大于所有可能的重试间隔（比如客户端最坏 3 天后重试，就要设 72 小时）；窗口内 key 被清理，重试会被当成新请求重复执行。&lt;/p&gt;&#xA;&lt;h3 id=&#34;25-第一层还差什么&#34;&gt;&lt;a href=&#34;#25-%e7%ac%ac%e4%b8%80%e5%b1%82%e8%bf%98%e5%b7%ae%e4%bb%80%e4%b9%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2.5 第一层还差什么&#xA;&lt;/h3&gt;&lt;p&gt;唯一ID 防住了&amp;quot;重复&amp;quot;，但防不住&amp;quot;乱序&amp;quot;和&amp;quot;并发&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;乱序回调到达时，唯一ID 只能保证&amp;quot;同一个回调只处理一次&amp;quot;，但两个不同的回调（支付成功、发货）谁先到谁后到，唯一ID 管不了。回调侧也没有业务方幂等键可用（回调请求由渠道生成，key 由渠道决定），所以状态机是回调幂等的兜底。并发改单时，两个不同的请求（改金额、取消）用的是不同的 key，唯一ID 也拦不住。&lt;/p&gt;&#xA;&lt;p&gt;所以需要第二层。&lt;/p&gt;&#xA;&lt;h2 id=&#34;3-第二层状态机防乱序&#34;&gt;&lt;a href=&#34;#3-%e7%ac%ac%e4%ba%8c%e5%b1%82%e7%8a%b6%e6%80%81%e6%9c%ba%e9%98%b2%e4%b9%b1%e5%ba%8f&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;3. 第二层：状态机防乱序&#xA;&lt;/h2&gt;&lt;h3 id=&#34;31-什么是状态机&#34;&gt;&lt;a href=&#34;#31-%e4%bb%80%e4%b9%88%e6%98%af%e7%8a%b6%e6%80%81%e6%9c%ba&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;3.1 什么是状态机&#xA;&lt;/h3&gt;&lt;p&gt;订单状态不是随便改的，只能按定义好的方向走。状态机就是给数据定义一套合法的状态迁移路径：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;pending（待支付）→ paid（已支付）→ shipped（已发货）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;pending → canceled（已取消）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;paid → pending&lt;/code&gt; 是非法迁移，已支付不能退回待支付。&lt;code&gt;shipped → paid&lt;/code&gt; 也是非法迁移，已发货不能改回已支付。状态机的作用就是：任何状态变更都先校验迁移合法性，非法的一律拒绝。&lt;/p&gt;&#xA;&lt;h3 id=&#34;32-状态机怎么做&#34;&gt;&lt;a href=&#34;#32-%e7%8a%b6%e6%80%81%e6%9c%ba%e6%80%8e%e4%b9%88%e5%81%9a&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;3.2 状态机怎么做&#xA;&lt;/h3&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;legalTransitions&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;State&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;State&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#x9;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Pending&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;   &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Paid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Canceled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#x9;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Paid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Shipped&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Refunding&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#x9;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Refunding&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Refunded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#x9;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Shipped&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;   &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#x9;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Canceled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#x9;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Refunded&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{},&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;valid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;to&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;State&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#x9;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;legalTransitions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;to&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;每次状态更新前，先跑 &lt;code&gt;valid(current, target)&lt;/code&gt;，不合法直接拒绝。&lt;/p&gt;&#xA;&lt;h3 id=&#34;33-实测数据&#34;&gt;&lt;a href=&#34;#33-%e5%ae%9e%e6%b5%8b%e6%95%b0%e6%8d%ae&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;3.3 实测数据&#xA;&lt;/h3&gt;&lt;p&gt;我模拟了支付回调乱序到达的场景（[实测 Go 1.26.2]）：业务顺序是&amp;quot;支付成功→发货&amp;quot;，但网络让发货回调先到了。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;对照组A（无状态机，直接改状态）:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  收到发货回调→shipped: pending → shipped&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  收到支付成功回调→paid: shipped → paid&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &amp;gt;&amp;gt;&amp;gt; 订单先被改成了已发货（支付还没成功），&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &amp;gt;&amp;gt;&amp;gt; 随后支付回调又把状态改回已支付——状态来回跳，&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &amp;gt;&amp;gt;&amp;gt; 中间出现过&amp;#34;未支付却已发货&amp;#34;的错误状态。&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;实验组B（有状态机，只允许合法迁移）:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  收到发货回调→shipped: pending → shipped ❌ 非法迁移被拒（订单保持 pending）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  收到支付成功回调→paid: pending → paid ✅ 合法迁移&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &amp;gt;&amp;gt;&amp;gt; 发货回调被拒（尚未支付不能发货），状态回到正确轨道&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对照组的状态来回跳，最终状态虽然回到了 paid，但中间出现过&amp;quot;未支付却已发货&amp;quot;。如果发货动作在状态变更的瞬间已经被触发（比如状态变更后立刻通知仓库），这个错误状态就是真实事故。实验组把非法迁移直接拒绝，乱序回调被消化，不会造成错误状态。&lt;/p&gt;&#xA;&lt;p&gt;状态机还有一个隐藏好处：&lt;strong&gt;重复回调天然幂等&lt;/strong&gt;。同一个 paid 回调到达三次，第一次 pending→paid 合法，后两次 paid→paid 不合法被忽略，重复被状态机顺带防住了。&#xA;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/idempotency-design/ch3-state-machine.png&#34; alt=&#34;订单状态机流转——单向轨道上的交通指挥&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;34-第二层还差什么&#34;&gt;&lt;a href=&#34;#34-%e7%ac%ac%e4%ba%8c%e5%b1%82%e8%bf%98%e5%b7%ae%e4%bb%80%e4%b9%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;3.4 第二层还差什么&#xA;&lt;/h3&gt;&lt;p&gt;状态机防住了&amp;quot;乱序&amp;quot;，但防不住&amp;quot;并发&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;两个并发请求同时读到订单状态是 paid，都想迁移到 refunding。状态机校验时都通过（paid→refunding 合法），然后两个都执行更新——后写覆盖先写，最终只生效一个，另一个静默丢失。状态机管的是&amp;quot;迁移路径合法&amp;quot;，管不了&amp;quot;两个请求同时迁移&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;第二层管不住并发，所以需要第三层。&lt;/p&gt;&#xA;&lt;h2 id=&#34;4-第三层乐观锁防并发&#34;&gt;&lt;a href=&#34;#4-%e7%ac%ac%e4%b8%89%e5%b1%82%e4%b9%90%e8%a7%82%e9%94%81%e9%98%b2%e5%b9%b6%e5%8f%91&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;4. 第三层：乐观锁防并发&#xA;&lt;/h2&gt;&lt;h3 id=&#34;41-什么是乐观锁&#34;&gt;&lt;a href=&#34;#41-%e4%bb%80%e4%b9%88%e6%98%af%e4%b9%90%e8%a7%82%e9%94%81&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;4.1 什么是乐观锁&#xA;&lt;/h3&gt;&lt;p&gt;乐观锁的思路是：更新时带上版本号，版本号对不上就拒绝。数据表加一个 version 字段，每次更新 &lt;code&gt;WHERE version = ?&lt;/code&gt; 且 &lt;code&gt;SET version = version + 1&lt;/code&gt;。如果影响行数是 0，说明版本号已变，有别人改过了——你的更新基于过期数据，拒绝。&lt;/p&gt;&#xA;&lt;p&gt;有个常见质疑：乐观锁冲突后要重试，失败率高，不如悲观锁（&lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt;）锁住行来得直接。这个质疑混淆了&amp;quot;冲突&amp;quot;和&amp;quot;失败&amp;quot;。乐观锁的冲突不是失败——是系统在告诉你&amp;quot;有并发&amp;quot;，重试是正常的业务路径；悲观锁是把并发串行化，靠等待消除冲突，代价是持有行锁期间所有其他操作排队。高并发下悲观锁的锁等待会放大延迟，乐观锁的失败重试通常是更优解。但重试不是免费的：冲突频繁（如热点行）时，重试会消耗连接与 CPU，此时悲观锁反而更简单——所以乐观锁适合&amp;quot;冲突偶尔发生&amp;quot;，悲观锁适合&amp;quot;冲突几乎必然&amp;quot;（比如两个请求必然改同一行）。&lt;/p&gt;&#xA;&lt;h3 id=&#34;42-乐观锁怎么做&#34;&gt;&lt;a href=&#34;#42-%e4%b9%90%e8%a7%82%e9%94%81%e6%80%8e%e4%b9%88%e5%81%9a&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;4.2 乐观锁怎么做&#xA;&lt;/h3&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- newVal 由应用侧基于旧快照计算（CAS 参数化写回）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;accounts&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;balance&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;version&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;version&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;+&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AND&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;version&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 读&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ver&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;acc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;balance&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;acc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;version&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 算&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;newVal&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;deduct&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// 写（UPDATE ... WHERE version = ?）&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;acc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;version&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;==&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ver&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#x9;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;acc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;balance&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;newVal&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#x9;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;acc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;version&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;++&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 版本不符 → 0 行更新 → 冲突&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;43-实测数据&#34;&gt;&lt;a href=&#34;#43-%e5%ae%9e%e6%b5%8b%e6%95%b0%e6%8d%ae&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;4.3 实测数据&#xA;&lt;/h3&gt;&lt;p&gt;我模拟了两组并发扣款（[实测 Go 1.26.2]）。对照组不做并发控制，read-modify-write 后直接写回；实验组用乐观锁，版本校验，冲突就重试：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;场景1（2笔200元并发扣款，余额1000，期望600）:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  对照组A: 最终余额 800，丢了一笔扣款（本应 600）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  实验组B: 最终余额 600，冲突重试 1 次，两笔都成功&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;场景2（10笔并发扣款，余额2000，期望0）:&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  对照组A: 最终余额 1800，只生效 1 笔，9 笔丢失&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  实验组B: 最终余额 0，冲突重试 45 次，10 笔全部生效&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对照组的数据很扎眼：10 笔并发扣款，9 笔静默丢失，每个请求都读到旧值，各自算完写回，后写覆盖先写。乐观锁把冲突显式暴露出来，全部扣款最终生效（实验中重试了 45 次）。生产环境必须设重试上限和退避策略（指数退避 + 抖动）：45 次是实验语义下&amp;quot;直到全部成功&amp;quot;的理想化结果，真实系统里超限应返回失败并告警，而不是无限重试。&#xA;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/idempotency-design/ch4-optimistic-lock.png&#34; alt=&#34;并发扣款冲突——两个请求抢写一本账&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;乐观锁的失败语义同样重要。冲突时不重试，直接失败：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;事务1 成功=true；事务2 成功=false（版本冲突，0 行更新）&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&amp;gt;&amp;gt;&amp;gt; 失败语义：返回 409/更新 0 行，上层决定重试或提示用户——不会静默丢扣款&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;乐观锁失败只是并发信号：收到失败信号，上层可以重试（如果业务允许）、可以提示用户、可以记日志。数据不会丢，只是需要处理。&lt;/p&gt;&#xA;&lt;h3 id=&#34;45-第三层还差什么&#34;&gt;&lt;a href=&#34;#45-%e7%ac%ac%e4%b8%89%e5%b1%82%e8%bf%98%e5%b7%ae%e4%bb%80%e4%b9%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;4.5 第三层还差什么&#xA;&lt;/h3&gt;&lt;p&gt;乐观锁防住了&amp;quot;并发改同一行&amp;quot;，但有个它管不了的窗口：&lt;strong&gt;先调外部渠道、后落库&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;真实支付系统里，退款有&amp;quot;先渠道后落库&amp;quot;和&amp;quot;先落库后渠道&amp;quot;两种做法。先渠道后落库是朴素方案：先调用支付渠道（打钱），成功后才更新本地状态。如果渠道调用成功、本地更新前进程崩溃，状态还是 paid——上层重试会再次调用渠道，重复退款。这个方案有缺陷，但恰好能暴露崩溃窗口；先落库后渠道（Saga 式状态标记）才是正确姿势，第 5.4 节会展开。乐观锁此时毫无办法，因为并发本身没有发生，是崩溃 + 重试。&lt;/p&gt;&#xA;&lt;p&gt;乐观锁的边界在外部调用上。&lt;/p&gt;&#xA;&lt;h2 id=&#34;5-层间配合真正的坑&#34;&gt;&lt;a href=&#34;#5-%e5%b1%82%e9%97%b4%e9%85%8d%e5%90%88%e7%9c%9f%e6%ad%a3%e7%9a%84%e5%9d%91&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;5. 层间配合：真正的坑&#xA;&lt;/h2&gt;&lt;h3 id=&#34;51-乐观锁--重试--重复退款&#34;&gt;&lt;a href=&#34;#51-%e4%b9%90%e8%a7%82%e9%94%81--%e9%87%8d%e8%af%95--%e9%87%8d%e5%a4%8d%e9%80%80%e6%ac%be&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;5.1 乐观锁 × 重试 = 重复退款&#xA;&lt;/h3&gt;&lt;p&gt;上面说的崩溃窗口，我专门做了实验验证（[实测 Go 1.26.2]）：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;场景：并发退款请求，第一个在&amp;#34;调渠道后、落库前&amp;#34;崩溃&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;对照组（无幂等键，盲目重试）: 渠道退款调用次数: 2&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;实验组（幂等键拦截重试）    : 渠道退款调用次数: 1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对照组把渠道退款调了两次——用户收到两笔退款，钱直接打错。这不是并发问题，是&amp;quot;崩溃后盲目重试&amp;quot;问题。乐观锁拦不住，因为数据没并发修改。这一层必须由幂等键（或状态机拒绝重复迁移）兜住。&lt;/p&gt;&#xA;&lt;p&gt;正确的重试姿势：重试时带上首次请求的唯一ID，幂等键层直接返回首次结果，渠道只被调一次。&#xA;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/idempotency-design/ch5-layer-conflict.png&#34; alt=&#34;层间冲突——崩溃窗口前拦住盲目重试&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h3 id=&#34;52-状态机--幂等键--谁先谁后&#34;&gt;&lt;a href=&#34;#52-%e7%8a%b6%e6%80%81%e6%9c%ba--%e5%b9%82%e7%ad%89%e9%94%ae--%e8%b0%81%e5%85%88%e8%b0%81%e5%90%8e&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;5.2 状态机 × 幂等键 = 谁先谁后&#xA;&lt;/h3&gt;&lt;p&gt;幂等键和状态机配合时有个顺序问题。同一笔退款请求重试，幂等键先查——已处理，直接返回首次结果。这个没问题。但反过来：&lt;strong&gt;先落库更新了状态，还没写幂等表，进程崩溃了&lt;/strong&gt;。重试到达，幂等键查不到，状态机校验发现订单已是 refunding，拒绝——结果正确（没有重复退款），但请求返回了&amp;quot;失败&amp;quot;，客户端会困惑。&lt;/p&gt;&#xA;&lt;p&gt;处理方式：状态机迁移和幂等表写入必须在同一个事务里，要么都成功，要么都回滚。层与层之间要在事务边界上咬合。&lt;/p&gt;&#xA;&lt;h3 id=&#34;53-幂等键过期窗口&#34;&gt;&lt;a href=&#34;#53-%e5%b9%82%e7%ad%89%e9%94%ae%e8%bf%87%e6%9c%9f%e7%aa%97%e5%8f%a3&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;5.3 幂等键过期窗口&#xA;&lt;/h3&gt;&lt;p&gt;第 2 章说过幂等键会过期清理。过期窗口内的重试是安全的（幂等键还在，直接返回首次结果）。&lt;strong&gt;窗口外的重试是危险的&lt;/strong&gt;——幂等键被清掉了，重试到达被当成新请求，重复执行。&lt;/p&gt;&#xA;&lt;p&gt;窗口怎么定？&lt;strong&gt;大于最长重试间隔，外加业务兜底&lt;/strong&gt;。Stripe 用 24 小时，你可以更长。但光靠窗口还不够——依赖幂等键的业务（比如退款）需要有兜底。对账的常见做法：定时任务按幂等键/订单号比对支付渠道流水与本地记录，发现两侧金额不一致即标记待查；自动冲正仅用于语义明确的场景（如确定是重复退款），否则转人工。幂等键是第一道闸，对账脚本挂在每天凌晨跑，比对两侧流水。&lt;/p&gt;&#xA;&lt;h3 id=&#34;54-三层配合的正确姿势&#34;&gt;&lt;a href=&#34;#54-%e4%b8%89%e5%b1%82%e9%85%8d%e5%90%88%e7%9a%84%e6%ad%a3%e7%a1%ae%e5%a7%bf%e5%8a%bf&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;5.4 三层配合的正确姿势&#xA;&lt;/h3&gt;&lt;p&gt;一个完整的写操作，三层是这样咬合的：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;唯一ID&lt;/strong&gt;：请求到达 → 查幂等表 → 已处理则返回首次结果&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;状态机&lt;/strong&gt;：未处理 → 校验当前状态 → 非法迁移拒绝&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;乐观锁&lt;/strong&gt;：合法迁移 → 版本校验更新（&lt;code&gt;WHERE version = ?&lt;/code&gt;）→ 冲突则重试或失败&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;事务咬合&lt;/strong&gt;：状态迁移 + 幂等表写入在同一事务，防&amp;quot;半成功&amp;quot;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;每一层解决自己那一类故障，又通过事务边界和重试策略互相兜底。这才是&amp;quot;三层防线&amp;quot;的完整含义。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/idempotency-design/ch5-three-layer-combo.png&#34; alt=&#34;三层配合的正确姿势——装配线检查员&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;再补一个细节：事务边界怎么划。状态迁移 + 幂等表写入放一个事务，是对的。但调用外部渠道（退款、发短信）不能放事务里——渠道调用是网络 IO，事务里做网络 IO 意味着持锁等网络，吞吐直接崩。正确姿势是：渠道调用放在事务外，用&amp;quot;状态标记 + 异步确认&amp;quot;模式：先落库&amp;quot;退款中&amp;quot;状态（事务内），再调渠道，渠道返回后更新为&amp;quot;已退款&amp;quot;（另一个事务）。这个模式下的崩溃窗口（调渠道后、更新状态前崩溃）正是第 5.1 节实验演示的场景，只能靠幂等键兜底。层间配合的功夫，一半在事务边界的划分上——事务划错了，三层叠在一起也照样出事。&lt;/p&gt;&#xA;&lt;h2 id=&#34;6-落地清单你的系统该上几层&#34;&gt;&lt;a href=&#34;#6-%e8%90%bd%e5%9c%b0%e6%b8%85%e5%8d%95%e4%bd%a0%e7%9a%84%e7%b3%bb%e7%bb%9f%e8%af%a5%e4%b8%8a%e5%87%a0%e5%b1%82&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;6. 落地清单：你的系统该上几层&#xA;&lt;/h2&gt;&lt;p&gt;不是所有系统都需要三层。判断标准很简单：&lt;strong&gt;你的写操作会遇到哪类故障？&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;场景&lt;/th&gt;&#xA;          &lt;th&gt;会遇到&lt;/th&gt;&#xA;          &lt;th&gt;需要&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;支付下单&lt;/td&gt;&#xA;          &lt;td&gt;重复✅ 乱序✅ 并发✅&lt;/td&gt;&#xA;          &lt;td&gt;三层&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;订单状态流转&lt;/td&gt;&#xA;          &lt;td&gt;重复✅ 乱序✅ 并发✅&lt;/td&gt;&#xA;          &lt;td&gt;三层&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;余额扣减&lt;/td&gt;&#xA;          &lt;td&gt;重复✅ 并发✅&lt;/td&gt;&#xA;          &lt;td&gt;唯一ID + 乐观锁&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;消息消费（at-least-once）&lt;/td&gt;&#xA;          &lt;td&gt;重复✅ 乱序△*&lt;/td&gt;&#xA;          &lt;td&gt;唯一ID + 业务侧版本判断&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;纯读操作&lt;/td&gt;&#xA;          &lt;td&gt;无&lt;/td&gt;&#xA;          &lt;td&gt;不需要&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;* 消息消费按分区保序（Kafka 同 key 消息在分区内有序），乱序只在跨分区场景出现，由业务侧版本号判断承担，不需要完整状态机。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;几个落地要点：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;重复是标配：任何写操作都要防重复，幂等键 + 唯一约束是底线&lt;/li&gt;&#xA;&lt;li&gt;有状态流转就要状态机：状态是数据的一部分，状态乱序会污染数据&lt;/li&gt;&#xA;&lt;li&gt;有并发写就上乐观锁：版本号字段成本极低，收益是&amp;quot;不静默丢更新&amp;quot;&lt;/li&gt;&#xA;&lt;li&gt;外部调用 + 本地落库，必须有层间咬合：崩溃窗口只能靠幂等键 + 事务兜底&lt;/li&gt;&#xA;&lt;li&gt;幂等键窗口 &amp;gt; 最长重试间隔，并配备对账兜底&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/idempotency-design/ch6-checklist.png&#34; alt=&#34;落地清单——你的系统该上几层&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;7-结语&#34;&gt;&lt;a href=&#34;#7-%e7%bb%93%e8%af%ad&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;7. 结语&#xA;&lt;/h2&gt;&lt;p&gt;三层防线，各管一类故障：唯一ID 防重复、状态机防乱序、乐观锁防并发。&lt;/p&gt;&#xA;&lt;p&gt;下次遇到&amp;quot;幂等&amp;quot;这两个字，问自己三个问题：我的操作会被重复吗？我的状态会乱序吗？我的数据会被并发覆盖吗？三个问题都回答了，再加上层间配合和对账兜底，你的系统才敢说自己是幂等的。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;你的系统里，哪个写操作还没上幂等？&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/idempotency-design/ch7-closing.png&#34; alt=&#34;结语——三层防线，各管一类故障&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;附录实验代码和原始数据&#34;&gt;&lt;a href=&#34;#%e9%99%84%e5%bd%95%e5%ae%9e%e9%aa%8c%e4%bb%a3%e7%a0%81%e5%92%8c%e5%8e%9f%e5%a7%8b%e6%95%b0%e6%8d%ae&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;附录：实验代码和原始数据&#xA;&lt;/h2&gt;&lt;p&gt;本文 4 组实验的代码和运行输出已开源：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;GitHub：&lt;a class=&#34;link&#34; href=&#34;https://github.com/wujiachen0727/zhiyulab-evidence/tree/main/idempotency-design&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;zhiyulab-evidence/idempotency-design&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;实验&lt;/th&gt;&#xA;          &lt;th&gt;目录&lt;/th&gt;&#xA;          &lt;th&gt;验证什么&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;E1 幂等键防重复&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a class=&#34;link&#34; href=&#34;https://github.com/wujiachen0727/zhiyulab-evidence/tree/main/idempotency-design/code/e1-idempotency-key&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;&lt;code&gt;e1-idempotency-key/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;同请求重发 100 次，无幂等键 99 笔重复入账，有幂等键 0 笔&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;E2 状态机防乱序&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a class=&#34;link&#34; href=&#34;https://github.com/wujiachen0727/zhiyulab-evidence/tree/main/idempotency-design/code/e2-state-machine&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;&lt;code&gt;e2-state-machine/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;乱序回调被拒、重复回调幂等&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;E3 乐观锁防并发&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a class=&#34;link&#34; href=&#34;https://github.com/wujiachen0727/zhiyulab-evidence/tree/main/idempotency-design/code/e3-optimistic-lock&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;&lt;code&gt;e3-optimistic-lock/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;10 笔并发扣款无锁丢 9 笔，乐观锁全生效（冲突重试 45 次）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;E4 层间冲突&lt;/td&gt;&#xA;          &lt;td&gt;&lt;a class=&#34;link&#34; href=&#34;https://github.com/wujiachen0727/zhiyulab-evidence/tree/main/idempotency-design/code/e4-layer-conflict&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;&lt;code&gt;e4-layer-conflict/&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#xA;          &lt;td&gt;崩溃后盲目重试=渠道退款 2 次，幂等键拦截=1 次&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;每个子目录都有独立 README 说明如何复现（&lt;code&gt;cd code/e{N}-{name}/ &amp;amp;&amp;amp; go run main.go&lt;/code&gt;，纯标准库无外部依赖）。二进制编译产物不入库，跑实验前自己 &lt;code&gt;go build&lt;/code&gt;。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;原文发布于 &lt;a class=&#34;link&#34; href=&#34;https://www.wujiachen.com.cn/posts/idempotency-design&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;止语Lab&lt;/a&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;</description>
        </item></channel>
</rss>
