分布式事务——在不可靠的网络上,把账算平
这是一份给一线工程师的分布式事务笔记:不背定义,从“为什么单机好好的,一拆成微服务就全是坑”讲起。先把单机事务的老底(WAL、两阶段锁、MVCC)翻出来,再看分布式把什么打碎了:时钟不可信、网络会丢包、机器会宕机。然后一个个拆主流解法——2PC/XA、TCC、Saga、本地消息表、Outbox+CDC、MQ事务消息、Seata,以及 Spanner、Percolator、Calvin 这些数据库原生的硬核路线。Mermaid 图直接渲染就能看,建议对照着代码和表结构一起读。
1. 痛从哪来:单机一句 commit,分布式一地鸡毛
单机时代,事务是数据库赏你的。一句 BEGIN; UPDATE account SET balance = balance – 100 WHERE id = A; UPDATE account SET balance = balance + 100 WHERE id = B; COMMIT;,要么全成,要么全不成。程序员不用操心磁盘半路断电、两个线程同时改一行怎么办,InnoDB 都兜了底。
微服务一把把它撕碎了。现在 A 的钱在 account-service 连的 MySQL 里,B 的钱在另一个库,订单在 order-service,库存要去调 stock-service 的 HTTP 接口。你写个“转账”业务,代码长这样:
// 伪代码,看着没毛病,跑起来全是雷
accountService.deduct(A, 100); // RPC 1:本地事务1,已提交
accountService.add(B, 100); // RPC 2:网络超时了,到底加上没?
orderService.createLog(...); // RPC 3:还没调,前两步能退吗?
单机事务靠“共享内存 + 共享磁盘 + 一个锁管理器”就能说了算。分布式下这三样全没了:
- 没有共享时钟:A 机器认为 12:00:01,B 机器可能是 12:00:07,你拿时间戳定谁先谁后,天然不准。
- 网络不是电线,是沼泽:超时不等于失败,对方可能已经执行完了,只是回包丢了。你重试,就可能执行两遍。
- 故障是常态:协调者挂了、参与者挂了、挂在一半的都有。单机 crash 重启靠 redo log 恢复就行,分布式 crash 完还要问别人“我上次干到哪了”。
所以分布式事务的本质问题就一句话:一群互不信任时钟、只能靠不可靠网络通信的节点,如何对“做还是不做”达成一致,并且故障后还能把账算平。
#mermaid-svg-Ouy0YjqRNDTnXIK6{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ouy0YjqRNDTnXIK6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .error-icon{fill:#552222;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .marker.cross{stroke:#333333;}#mermaid-svg-Ouy0YjqRNDTnXIK6 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ouy0YjqRNDTnXIK6 p{margin:0;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .cluster-label text{fill:#333;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .cluster-label span{color:#333;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .cluster-label span p{background-color:transparent;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .label text,#mermaid-svg-Ouy0YjqRNDTnXIK6 span{fill:#333;color:#333;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .node rect,#mermaid-svg-Ouy0YjqRNDTnXIK6 .node circle,#mermaid-svg-Ouy0YjqRNDTnXIK6 .node ellipse,#mermaid-svg-Ouy0YjqRNDTnXIK6 .node polygon,#mermaid-svg-Ouy0YjqRNDTnXIK6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .rough-node .label text,#mermaid-svg-Ouy0YjqRNDTnXIK6 .node .label text,#mermaid-svg-Ouy0YjqRNDTnXIK6 .image-shape .label,#mermaid-svg-Ouy0YjqRNDTnXIK6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .rough-node .label,#mermaid-svg-Ouy0YjqRNDTnXIK6 .node .label,#mermaid-svg-Ouy0YjqRNDTnXIK6 .image-shape .label,#mermaid-svg-Ouy0YjqRNDTnXIK6 .icon-shape .label{text-align:center;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .node.clickable{cursor:pointer;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .arrowheadPath{fill:#333333;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ouy0YjqRNDTnXIK6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ouy0YjqRNDTnXIK6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ouy0YjqRNDTnXIK6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .cluster text{fill:#333;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .cluster span{color:#333;}#mermaid-svg-Ouy0YjqRNDTnXIK6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ouy0YjqRNDTnXIK6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .icon-shape,#mermaid-svg-Ouy0YjqRNDTnXIK6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .icon-shape p,#mermaid-svg-Ouy0YjqRNDTnXIK6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .icon-shape .label rect,#mermaid-svg-Ouy0YjqRNDTnXIK6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ouy0YjqRNDTnXIK6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ouy0YjqRNDTnXIK6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ouy0YjqRNDTnXIK6 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
拆分之后
分布式事务:三个各怀心思的人
RPC/HTTP可能超时、重发、乱序
RPC/HTTP
RPC/HTTP
各自本地事务各管各的提交
各自本地事务
协调者order-service
参与者1account-db
参与者2stock-service
参与者3MQ / 第三方支付
库1
库2
单机事务:一个说了算的人
应用线程
锁管理器 + Buffer Pool + WAL
本地磁盘Redo / Undo 一盘棋
这个图值得多看一眼:左边是“集中式大脑”,右边是“联邦制”。分布式事务的所有方案,都是在回答联邦制下怎么不打内战。答案无非两派:要么强行选个中央集权(2PC/XA/Spanner),要么承认分治、用补偿和重试最终抹平(TCC/Saga/消息最终一致)。后面十几章,全是这两派的变体。
2. 先补课:ACID、CAP、BASE 到底在说什么
面试八股文里这三个词被嚼烂了,但真到选型时,很多人还是把它们当口号。这里用人话重新讲一遍,讲清楚它们管什么、不管什么。
2.1 ACID:数据库给你签的合同
- A(Atomicity,原子性):一组操作要么全生效,要么全不生效。转账扣了 A 没加给 B,就是违约。
- C(Consistency,一致性):这个最被误解。它不是指“所有副本一样”,而是指“数据满足业务 invariant”。比如 A+B 总额不变、库存不能为负。注意,C 是 A+I 共同保证的结果,数据库自己保证不了业务规则写错了。
- I(Isolation,隔离性):并发事务互相看不见中间状态。你转账到一半,别人查账不能看到 A 少了、B 还没多。
- D(Durability,持久性):一旦 commit,掉电也不丢。靠 redo log + 刷盘保证。
单机下这四条是数据库内核用锁 + 日志硬扛出来的。分布式下,A 和 I 最难,因为操作散在多台机器上,锁不住,也不好一起回滚。
2.2 CAP:不是三选二的选择题,是取舍题
CAP 说:在网络分区(P)发生时,你只能在一致性(C)和可用性(A)之间二选一。很多人理解成“平时可以三者兼得”,错了。分布式系统设计者 Eric Brewer 后来自己澄清:CAP 的真相是 P 一定会来,你平时就要为 P 做准备。
举个电商例子:下单要扣库存,库存服务和订单服务之间网络断了。你怎么办?
- 选 C:订单服务直接报错“库存未知,稍后再试”。数据不会错,但用户骂娘,可用性掉了。
- 选 A:先让用户下单成功,库存显示“可能有货”,后台慢慢对账。体验保住了,但可能超卖,要事后补偿。
互联网绝大多数核心链路选 A + 最终一致,只有钱、券、库存扣减那一下,局部选 C。这就是 BASE 的由来。
2.3 BASE:把要求放 low 一点,日子好过一点
BASE 是 eBay 那帮人总结的野路子,和 ACID 逐条对应:
- BA(Basically Available,基本可用):允许部分失败,降级、限流、返回兜底数据都行,别整体挂。
- S(Soft state,软状态):允许中间状态存在。比如订单先是 PAYING,几秒后才变 PAID,中间查到什么都别惊讶。
- E(Eventually consistent,最终一致):不要求读到最新,但保证在没有新写入后,最终所有副本收敛到一样。至于“最终”是 50ms 还是 5 分钟,看方案。
一句话总结:ACID 是“当场算清”,BASE 是“先记小本本,事后算清”。 后面讲的 Saga、TCC、消息事务,全是“小本本” 的不同记法。
#mermaid-svg-hGcUb1A1tyhEg7yE{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hGcUb1A1tyhEg7yE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hGcUb1A1tyhEg7yE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hGcUb1A1tyhEg7yE .error-icon{fill:#552222;}#mermaid-svg-hGcUb1A1tyhEg7yE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hGcUb1A1tyhEg7yE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hGcUb1A1tyhEg7yE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hGcUb1A1tyhEg7yE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hGcUb1A1tyhEg7yE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hGcUb1A1tyhEg7yE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hGcUb1A1tyhEg7yE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hGcUb1A1tyhEg7yE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hGcUb1A1tyhEg7yE .marker.cross{stroke:#333333;}#mermaid-svg-hGcUb1A1tyhEg7yE svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hGcUb1A1tyhEg7yE p{margin:0;}#mermaid-svg-hGcUb1A1tyhEg7yE .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-hGcUb1A1tyhEg7yE .cluster-label text{fill:#333;}#mermaid-svg-hGcUb1A1tyhEg7yE .cluster-label span{color:#333;}#mermaid-svg-hGcUb1A1tyhEg7yE .cluster-label span p{background-color:transparent;}#mermaid-svg-hGcUb1A1tyhEg7yE .label text,#mermaid-svg-hGcUb1A1tyhEg7yE span{fill:#333;color:#333;}#mermaid-svg-hGcUb1A1tyhEg7yE .node rect,#mermaid-svg-hGcUb1A1tyhEg7yE .node circle,#mermaid-svg-hGcUb1A1tyhEg7yE .node ellipse,#mermaid-svg-hGcUb1A1tyhEg7yE .node polygon,#mermaid-svg-hGcUb1A1tyhEg7yE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hGcUb1A1tyhEg7yE .rough-node .label text,#mermaid-svg-hGcUb1A1tyhEg7yE .node .label text,#mermaid-svg-hGcUb1A1tyhEg7yE .image-shape .label,#mermaid-svg-hGcUb1A1tyhEg7yE .icon-shape .label{text-anchor:middle;}#mermaid-svg-hGcUb1A1tyhEg7yE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hGcUb1A1tyhEg7yE .rough-node .label,#mermaid-svg-hGcUb1A1tyhEg7yE .node .label,#mermaid-svg-hGcUb1A1tyhEg7yE .image-shape .label,#mermaid-svg-hGcUb1A1tyhEg7yE .icon-shape .label{text-align:center;}#mermaid-svg-hGcUb1A1tyhEg7yE .node.clickable{cursor:pointer;}#mermaid-svg-hGcUb1A1tyhEg7yE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hGcUb1A1tyhEg7yE .arrowheadPath{fill:#333333;}#mermaid-svg-hGcUb1A1tyhEg7yE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hGcUb1A1tyhEg7yE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hGcUb1A1tyhEg7yE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hGcUb1A1tyhEg7yE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hGcUb1A1tyhEg7yE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hGcUb1A1tyhEg7yE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hGcUb1A1tyhEg7yE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hGcUb1A1tyhEg7yE .cluster text{fill:#333;}#mermaid-svg-hGcUb1A1tyhEg7yE .cluster span{color:#333;}#mermaid-svg-hGcUb1A1tyhEg7yE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hGcUb1A1tyhEg7yE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hGcUb1A1tyhEg7yE rect.text{fill:none;stroke-width:0;}#mermaid-svg-hGcUb1A1tyhEg7yE .icon-shape,#mermaid-svg-hGcUb1A1tyhEg7yE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hGcUb1A1tyhEg7yE .icon-shape p,#mermaid-svg-hGcUb1A1tyhEg7yE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hGcUb1A1tyhEg7yE .icon-shape .label rect,#mermaid-svg-hGcUb1A1tyhEg7yE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hGcUb1A1tyhEg7yE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hGcUb1A1tyhEg7yE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hGcUb1A1tyhEg7yE :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
网络分区时做不到
BASE:先记账,事后抹平
扣 A 100 成功记流水 TRYING
调 B 加钱超时未知
后台重试 / 对账 / 补偿最终两边平
ACID:当场算清
扣 A 100
加 B 100
一起提交失败一起滚
2.4 一致性光谱:别再只说“强一致 / 最终一致”
工程上一致性是个光谱,从强到弱大致是:线性一致 > 顺序一致 > 因果一致 > 会话一致 > 最终一致。选型时问自己两个问题就行:
转账、证券撮合要线性一致;商品详情页、Feed 流,最终一致加缓存就够了。分布式事务方案,本质就是在这个光谱上选个落点。
3. 单机事务的老底:WAL、锁与 MVCC
不懂单机,就看不懂分布式为什么难。这一章是地基, already 懂 InnoDB 的可以快速翻,但别跳——后面 Seata AT、Percolator 全是对这些老招式的分布式翻版。
3.1 Undo + Redo + WAL:数据库不丢数据的三板斧
数据库敢说 D(持久),靠的不是每次 commit 都把数据页刷盘(那太慢了),而是 WAL(Write-Ahead Logging,预写日志):改数据前,先把“我要干什么”顺序写进日志并刷盘,数据页慢慢刷都行。crash 了重启回放日志即可。
- Redo log:记“做过什么”,用来 crash 后重做,保证已提交不丢。顺序写,快。
- Undo log:记“以前什么样”,用来回滚和给别的事务提供旧版本快照。
- Checkpoint:定期把脏页刷盘,截断日志,不然日志无限长,恢复要回放到天荒地老。
#mermaid-svg-COuS1nbqf10qP8dC{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-COuS1nbqf10qP8dC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-COuS1nbqf10qP8dC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-COuS1nbqf10qP8dC .error-icon{fill:#552222;}#mermaid-svg-COuS1nbqf10qP8dC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-COuS1nbqf10qP8dC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-COuS1nbqf10qP8dC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-COuS1nbqf10qP8dC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-COuS1nbqf10qP8dC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-COuS1nbqf10qP8dC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-COuS1nbqf10qP8dC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-COuS1nbqf10qP8dC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-COuS1nbqf10qP8dC .marker.cross{stroke:#333333;}#mermaid-svg-COuS1nbqf10qP8dC svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-COuS1nbqf10qP8dC p{margin:0;}#mermaid-svg-COuS1nbqf10qP8dC .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-COuS1nbqf10qP8dC .cluster-label text{fill:#333;}#mermaid-svg-COuS1nbqf10qP8dC .cluster-label span{color:#333;}#mermaid-svg-COuS1nbqf10qP8dC .cluster-label span p{background-color:transparent;}#mermaid-svg-COuS1nbqf10qP8dC .label text,#mermaid-svg-COuS1nbqf10qP8dC span{fill:#333;color:#333;}#mermaid-svg-COuS1nbqf10qP8dC .node rect,#mermaid-svg-COuS1nbqf10qP8dC .node circle,#mermaid-svg-COuS1nbqf10qP8dC .node ellipse,#mermaid-svg-COuS1nbqf10qP8dC .node polygon,#mermaid-svg-COuS1nbqf10qP8dC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-COuS1nbqf10qP8dC .rough-node .label text,#mermaid-svg-COuS1nbqf10qP8dC .node .label text,#mermaid-svg-COuS1nbqf10qP8dC .image-shape .label,#mermaid-svg-COuS1nbqf10qP8dC .icon-shape .label{text-anchor:middle;}#mermaid-svg-COuS1nbqf10qP8dC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-COuS1nbqf10qP8dC .rough-node .label,#mermaid-svg-COuS1nbqf10qP8dC .node .label,#mermaid-svg-COuS1nbqf10qP8dC .image-shape .label,#mermaid-svg-COuS1nbqf10qP8dC .icon-shape .label{text-align:center;}#mermaid-svg-COuS1nbqf10qP8dC .node.clickable{cursor:pointer;}#mermaid-svg-COuS1nbqf10qP8dC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-COuS1nbqf10qP8dC .arrowheadPath{fill:#333333;}#mermaid-svg-COuS1nbqf10qP8dC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-COuS1nbqf10qP8dC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-COuS1nbqf10qP8dC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-COuS1nbqf10qP8dC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-COuS1nbqf10qP8dC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-COuS1nbqf10qP8dC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-COuS1nbqf10qP8dC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-COuS1nbqf10qP8dC .cluster text{fill:#333;}#mermaid-svg-COuS1nbqf10qP8dC .cluster span{color:#333;}#mermaid-svg-COuS1nbqf10qP8dC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-COuS1nbqf10qP8dC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-COuS1nbqf10qP8dC rect.text{fill:none;stroke-width:0;}#mermaid-svg-COuS1nbqf10qP8dC .icon-shape,#mermaid-svg-COuS1nbqf10qP8dC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-COuS1nbqf10qP8dC .icon-shape p,#mermaid-svg-COuS1nbqf10qP8dC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-COuS1nbqf10qP8dC .icon-shape .label rect,#mermaid-svg-COuS1nbqf10qP8dC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-COuS1nbqf10qP8dC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-COuS1nbqf10qP8dC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-COuS1nbqf10qP8dC :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否 / 回滚
是
否
事务:UPDATE balance = 900 WHERE id=A
1. 写 Undo:记下旧值 1000
2. 改 Buffer Pool 内存页:1000 -> 900
3. 写 Redo Log Buffer:记下新值 900
COMMIT?
刷 Redo 进磁盘即可返回成功数据页稍后刷
用 Undo 把内存页改回 1000
crash 了?
重启:用 Redo 重做已提交用 Undo 回滚未提交
后台刷脏页 + Checkpoint
理解这张图的意义在于:后面所有分布式方案,都在模仿它。2PC 的 prepare 阶段就是分布式版的“写日志但不提交”;Saga 的补偿表就是分布式版的“undo log”;Outbox 表就是分布式版的“redo log”。换了身衣服,魂没变。
3.2 隔离性:并发打架时,谁先动手
两个事务同时动同一行,无非三种冲突:脏读(读到别人没提交的)、不可重复读(两次读同一行结果变了)、幻读(两次查范围行数变了)。SQL 标准定了四个隔离级别,本质是“加锁的狠度”:
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 基本不加锁 |
| READ COMMITTED | 不会 | 可能 | 可能 | 写锁 + 读已提交快照 |
| REPEATABLE READ(MySQL 默认) | 不会 | 不会 | 部分防(靠间隙锁) | MVCC 快照 + 间隙锁 |
| SERIALIZABLE | 不会 | 不会 | 不会 | 全加锁串行,性能最差 |
MySQL InnoDB 默认 REPEATABLE READ,靠两招:2PL(两阶段锁)管写,MVCC(多版本并发控制)管读。写要加行锁,读直接读快照,不互相堵,这就是读写不阻塞的秘密。
MVCC 一句话原理:每行数据带两个隐藏版本号 trx_id(谁创建的)、roll_ptr(指向上个版本)。读的时候只看在你事务开始前就提交了的版本,新版本对你不可见。所以你在事务里反复 SELECT,看到的是同一份快照。
但 MVCC 有个著名的坑:写偏斜(Write Skew)。医生值班例子:规定至少留一个医生,两个事务同时查“在岗几人”,都看到 2 人,各自请假一人,都成功了,结果 0 人在岗。RR 快照下两个读都不冲突,写也不冲突,数据库觉得没问题,业务 invariant 却破了。这就是为什么银行、库存场景,光靠数据库隔离级别不够,还要在应用层加乐观锁(version 列)或悲观锁(SELECT … FOR UPDATE)。
分布式把这个问题放大十倍:锁散在多个库,MVCC 快照各管各的,没有全局 trx_id。后面讲 Spanner 用 TrueTime 造全局时间戳、TiDB 用 TSO 造全局递增号,都是为了解决“分布式下谁先谁后”。
4. 强一致派:2PC、3PC 与 XA
强一致派的思路很直:找个老大(协调者),所有人听他的。老大说“都准备好没”,大家说“好了”,老大再说“提交”。这就是两阶段提交(2PC)。
4.1 2PC 全流程:准备 + 提交,听着简单,坑全在细节
角色就两个:协调者(Coordinator) 和 参与者(Participant)。正常流程分两阶段:
参与者 RM2(stock-db)参与者 RM1(account-db)协调者 TM参与者 RM2(stock-db)参与者 RM1(account-db)协调者 TM#mermaid-svg-SP0Zk8wzd44MIw6s{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SP0Zk8wzd44MIw6s .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SP0Zk8wzd44MIw6s .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SP0Zk8wzd44MIw6s .error-icon{fill:#552222;}#mermaid-svg-SP0Zk8wzd44MIw6s .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SP0Zk8wzd44MIw6s .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SP0Zk8wzd44MIw6s .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SP0Zk8wzd44MIw6s .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SP0Zk8wzd44MIw6s .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SP0Zk8wzd44MIw6s .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SP0Zk8wzd44MIw6s .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SP0Zk8wzd44MIw6s .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SP0Zk8wzd44MIw6s .marker.cross{stroke:#333333;}#mermaid-svg-SP0Zk8wzd44MIw6s svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SP0Zk8wzd44MIw6s p{margin:0;}#mermaid-svg-SP0Zk8wzd44MIw6s .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SP0Zk8wzd44MIw6s text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-SP0Zk8wzd44MIw6s .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-SP0Zk8wzd44MIw6s .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-SP0Zk8wzd44MIw6s .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-SP0Zk8wzd44MIw6s .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-SP0Zk8wzd44MIw6s #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-SP0Zk8wzd44MIw6s .sequenceNumber{fill:white;}#mermaid-svg-SP0Zk8wzd44MIw6s #sequencenumber{fill:#333;}#mermaid-svg-SP0Zk8wzd44MIw6s #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-SP0Zk8wzd44MIw6s .messageText{fill:#333;stroke:none;}#mermaid-svg-SP0Zk8wzd44MIw6s .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SP0Zk8wzd44MIw6s .labelText,#mermaid-svg-SP0Zk8wzd44MIw6s .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-SP0Zk8wzd44MIw6s .loopText,#mermaid-svg-SP0Zk8wzd44MIw6s .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-SP0Zk8wzd44MIw6s .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-SP0Zk8wzd44MIw6s .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-SP0Zk8wzd44MIw6s .noteText,#mermaid-svg-SP0Zk8wzd44MIw6s .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-SP0Zk8wzd44MIw6s .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SP0Zk8wzd44MIw6s .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SP0Zk8wzd44MIw6s .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-SP0Zk8wzd44MIw6s .actorPopupMenu{position:absolute;}#mermaid-svg-SP0Zk8wzd44MIw6s .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-SP0Zk8wzd44MIw6s .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-SP0Zk8wzd44MIw6s .actor-man circle,#mermaid-svg-SP0Zk8wzd44MIw6s line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-SP0Zk8wzd44MIw6s :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}阶段1:Prepare(投票)阶段2:Commit(执行)prepare(XID)1prepare(XID)2写 undo/redo + 拿行锁但不提交3写 undo/redo + 拿行锁但不提交4YES(我准备好了)5YES(我准备好了)6都 YES,写 commit 日志7commit8commit9真正提交,释放锁10真正提交,释放锁11ACK12ACK13
看着没毛病,但魔鬼在异常分支。画个状态机你就明白为什么 2PC 被吐槽是“阻塞协议”:
#mermaid-svg-0Tg0bgfRrMCxzDNn{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0Tg0bgfRrMCxzDNn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0Tg0bgfRrMCxzDNn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0Tg0bgfRrMCxzDNn .error-icon{fill:#552222;}#mermaid-svg-0Tg0bgfRrMCxzDNn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0Tg0bgfRrMCxzDNn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0Tg0bgfRrMCxzDNn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0Tg0bgfRrMCxzDNn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0Tg0bgfRrMCxzDNn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0Tg0bgfRrMCxzDNn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0Tg0bgfRrMCxzDNn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0Tg0bgfRrMCxzDNn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0Tg0bgfRrMCxzDNn .marker.cross{stroke:#333333;}#mermaid-svg-0Tg0bgfRrMCxzDNn svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0Tg0bgfRrMCxzDNn p{margin:0;}#mermaid-svg-0Tg0bgfRrMCxzDNn defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-0Tg0bgfRrMCxzDNn g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-0Tg0bgfRrMCxzDNn g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-0Tg0bgfRrMCxzDNn g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-0Tg0bgfRrMCxzDNn g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-0Tg0bgfRrMCxzDNn g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-0Tg0bgfRrMCxzDNn .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-0Tg0bgfRrMCxzDNn .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-0Tg0bgfRrMCxzDNn .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-0Tg0bgfRrMCxzDNn .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0Tg0bgfRrMCxzDNn .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-0Tg0bgfRrMCxzDNn .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-0Tg0bgfRrMCxzDNn .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-0Tg0bgfRrMCxzDNn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0Tg0bgfRrMCxzDNn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0Tg0bgfRrMCxzDNn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0Tg0bgfRrMCxzDNn .edgeLabel .label text{fill:#333;}#mermaid-svg-0Tg0bgfRrMCxzDNn .label div .edgeLabel{color:#333;}#mermaid-svg-0Tg0bgfRrMCxzDNn .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-0Tg0bgfRrMCxzDNn .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-0Tg0bgfRrMCxzDNn .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-0Tg0bgfRrMCxzDNn .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-0Tg0bgfRrMCxzDNn .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-0Tg0bgfRrMCxzDNn .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0Tg0bgfRrMCxzDNn .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0Tg0bgfRrMCxzDNn #statediagram-barbEnd{fill:#333333;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0Tg0bgfRrMCxzDNn .cluster-label,#mermaid-svg-0Tg0bgfRrMCxzDNn .nodeLabel{color:#131300;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-0Tg0bgfRrMCxzDNn .note-edge{stroke-dasharray:5;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-note text{fill:black;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram-note .nodeLabel{color:black;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagram .edgeLabel{color:red;}#mermaid-svg-0Tg0bgfRrMCxzDNn #dependencyStart,#mermaid-svg-0Tg0bgfRrMCxzDNn #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-0Tg0bgfRrMCxzDNn .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0Tg0bgfRrMCxzDNn :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
事务开始
协调者发 prepare
参与者回 YES进入不确定期,锁着资源等
任一回 NO / 超时回滚释放锁
收到 commit,提交
收到 rollback,回滚
commit/rollback 包丢了或协调者挂了一直 holding 锁,谁都动不了这行
重连问到 commit
重连问到 abort
长时间问不到,只能人工介入
Init
Preparing
Prepared
Aborted
Committed
Blocked
两个致命伤:
生产血泪:MySQL 的 XA + 跨库事务,prepare 后连接断了,那个 prepare 的事务会一直占着锁,SHOW ENGINE INNODB STATUS 里能看到一大堆 PREPARED 悬着,DBA 半夜被叫起来 XA RECOVER + 手工 XA COMMIT/ROLLBACK。用过的人都懂。
4.2 XA 与 JTA:Java 世界的 2PC 标准件
XA 是 X/Open 定的分布式事务接口,JTA 是 Java 对它的封装。角色分三个:AP(你的业务代码)、TM(Transaction Manager,协调者,如 Atomikos、Narayana)、RM(Resource Manager,各数据库)。
// JTA 写起来很爽,跟本地事务几乎一样
UserTransaction utx = (UserTransaction) ctx.lookup("java:comp/UserTransaction");
utx.begin();
orderDao.insert(order); // 库1,自动 enlist 为分支
accountDao.deduct(a, 100); // 库2,自动 enlist 为分支
utx.commit(); // TM 替你跑完 2PC
爽的代价是:所有 RM 必须是支持 XA 的关系库,TM 和 RM 之间长连接,锁持有时间 = 两次网络往返 + 业务执行时间。微服务时代服务之间是 HTTP,不是数据库直连,XA 插不进去。所以 XA 在单体 + 多库、同机房场景还能打,跨服务、跨公网基本出局。
4.3 3PC:想解决阻塞,结果引入更多坑
3PC 加了个 CanCommit 询问阶段和超时自动 abort,想让参与者在协调者挂了时能自己超时释放锁。理论上减少阻塞,实际上:
- 多了一次网络往返,延迟更高。
- 超时自动 abort 会和协调者的 commit 决定分叉(参与者超时自杀了,协调者其实想提交),一致性反而更差。
- 依然解决不了分区下的脑裂。
所以 3PC 基本只活在教科书里,工业界没人用。记住结论就行:想靠加阶段解决 2PC 的阻塞,是死胡同,真正的出路是换思路(Paxos/Raft 共识)或降要求(Saga/消息最终一致)。
4.4 数据流图:2PC 的日志先行
2PC 正确性的命根子是“日志先行”:协调者在发 commit 前必须先把决定落盘,参与者在回 YES 前必须把 undo/redo 落盘。这样任何一方挂了重启,都能从盘里找回决定。
#mermaid-svg-hBjdKwRmvN8A0xzt{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hBjdKwRmvN8A0xzt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hBjdKwRmvN8A0xzt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hBjdKwRmvN8A0xzt .error-icon{fill:#552222;}#mermaid-svg-hBjdKwRmvN8A0xzt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hBjdKwRmvN8A0xzt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hBjdKwRmvN8A0xzt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hBjdKwRmvN8A0xzt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hBjdKwRmvN8A0xzt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hBjdKwRmvN8A0xzt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hBjdKwRmvN8A0xzt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hBjdKwRmvN8A0xzt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hBjdKwRmvN8A0xzt .marker.cross{stroke:#333333;}#mermaid-svg-hBjdKwRmvN8A0xzt svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hBjdKwRmvN8A0xzt p{margin:0;}#mermaid-svg-hBjdKwRmvN8A0xzt .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-hBjdKwRmvN8A0xzt .cluster-label text{fill:#333;}#mermaid-svg-hBjdKwRmvN8A0xzt .cluster-label span{color:#333;}#mermaid-svg-hBjdKwRmvN8A0xzt .cluster-label span p{background-color:transparent;}#mermaid-svg-hBjdKwRmvN8A0xzt .label text,#mermaid-svg-hBjdKwRmvN8A0xzt span{fill:#333;color:#333;}#mermaid-svg-hBjdKwRmvN8A0xzt .node rect,#mermaid-svg-hBjdKwRmvN8A0xzt .node circle,#mermaid-svg-hBjdKwRmvN8A0xzt .node ellipse,#mermaid-svg-hBjdKwRmvN8A0xzt .node polygon,#mermaid-svg-hBjdKwRmvN8A0xzt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hBjdKwRmvN8A0xzt .rough-node .label text,#mermaid-svg-hBjdKwRmvN8A0xzt .node .label text,#mermaid-svg-hBjdKwRmvN8A0xzt .image-shape .label,#mermaid-svg-hBjdKwRmvN8A0xzt .icon-shape .label{text-anchor:middle;}#mermaid-svg-hBjdKwRmvN8A0xzt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hBjdKwRmvN8A0xzt .rough-node .label,#mermaid-svg-hBjdKwRmvN8A0xzt .node .label,#mermaid-svg-hBjdKwRmvN8A0xzt .image-shape .label,#mermaid-svg-hBjdKwRmvN8A0xzt .icon-shape .label{text-align:center;}#mermaid-svg-hBjdKwRmvN8A0xzt .node.clickable{cursor:pointer;}#mermaid-svg-hBjdKwRmvN8A0xzt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hBjdKwRmvN8A0xzt .arrowheadPath{fill:#333333;}#mermaid-svg-hBjdKwRmvN8A0xzt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hBjdKwRmvN8A0xzt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hBjdKwRmvN8A0xzt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hBjdKwRmvN8A0xzt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hBjdKwRmvN8A0xzt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hBjdKwRmvN8A0xzt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hBjdKwRmvN8A0xzt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hBjdKwRmvN8A0xzt .cluster text{fill:#333;}#mermaid-svg-hBjdKwRmvN8A0xzt .cluster span{color:#333;}#mermaid-svg-hBjdKwRmvN8A0xzt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hBjdKwRmvN8A0xzt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hBjdKwRmvN8A0xzt rect.text{fill:none;stroke-width:0;}#mermaid-svg-hBjdKwRmvN8A0xzt .icon-shape,#mermaid-svg-hBjdKwRmvN8A0xzt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hBjdKwRmvN8A0xzt .icon-shape p,#mermaid-svg-hBjdKwRmvN8A0xzt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hBjdKwRmvN8A0xzt .icon-shape .label rect,#mermaid-svg-hBjdKwRmvN8A0xzt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hBjdKwRmvN8A0xzt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hBjdKwRmvN8A0xzt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hBjdKwRmvN8A0xzt :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
参与者 RM
协调者 TM
全 YES
有 NO/超时
commit
rollback
超时没收到
写 start 日志
发 prepare
收齐 YES?
刷 commit 日志到盘再发 commit
刷 abort 日志再发 rollback
收 prepare
写 ready 日志 + 占锁
回 YES
收 commit?
提交 + 写 commit 日志
用 undo 回滚
卡住,定时问协调者不能擅自决定
5. 共识派:Paxos、Raft 与数据库原生分布式事务
2PC 是“老大说了算”,老大挂了全完。共识算法(Paxos/Raft)是“多数派说了算”,挂掉少数还能活。这是两条完全不同的技术线,别搞混:2PC 管“原子提交”,共识管“多副本选主与日志复制”。Spanner 这类 NewSQL 厉害在把两者拼起来了。
5.1 为什么需要共识:两将军问题与 FLP
两将军问题证明了:在不可靠网络上,没有“两人同时确信对方会进攻”的确定性协议。FLP 不可能定理更狠:在异步网络 + 容忍一个 crash 节点时,不存在同时保证安全性和活性的确定性共识。工程翻译:别想造一个又快、又准、又不怕挂的完美协议,只能在超时、重试、多数派之间找平衡。
Raft 就是这个平衡的优雅实现:Leader 选举 + 日志复制 + 多数派提交。写请求只打给 Leader,Leader 把 log 发给 Follower,过半 ack 就算提交,再异步应用。这保证了即使挂掉少数节点,已提交的不丢、顺序不乱。
#mermaid-svg-6v3HYJS3AWeMgv1d{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6v3HYJS3AWeMgv1d .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6v3HYJS3AWeMgv1d .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6v3HYJS3AWeMgv1d .error-icon{fill:#552222;}#mermaid-svg-6v3HYJS3AWeMgv1d .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6v3HYJS3AWeMgv1d .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6v3HYJS3AWeMgv1d .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6v3HYJS3AWeMgv1d .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6v3HYJS3AWeMgv1d .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6v3HYJS3AWeMgv1d .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6v3HYJS3AWeMgv1d .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6v3HYJS3AWeMgv1d .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6v3HYJS3AWeMgv1d .marker.cross{stroke:#333333;}#mermaid-svg-6v3HYJS3AWeMgv1d svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6v3HYJS3AWeMgv1d p{margin:0;}#mermaid-svg-6v3HYJS3AWeMgv1d .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-6v3HYJS3AWeMgv1d .cluster-label text{fill:#333;}#mermaid-svg-6v3HYJS3AWeMgv1d .cluster-label span{color:#333;}#mermaid-svg-6v3HYJS3AWeMgv1d .cluster-label span p{background-color:transparent;}#mermaid-svg-6v3HYJS3AWeMgv1d .label text,#mermaid-svg-6v3HYJS3AWeMgv1d span{fill:#333;color:#333;}#mermaid-svg-6v3HYJS3AWeMgv1d .node rect,#mermaid-svg-6v3HYJS3AWeMgv1d .node circle,#mermaid-svg-6v3HYJS3AWeMgv1d .node ellipse,#mermaid-svg-6v3HYJS3AWeMgv1d .node polygon,#mermaid-svg-6v3HYJS3AWeMgv1d .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6v3HYJS3AWeMgv1d .rough-node .label text,#mermaid-svg-6v3HYJS3AWeMgv1d .node .label text,#mermaid-svg-6v3HYJS3AWeMgv1d .image-shape .label,#mermaid-svg-6v3HYJS3AWeMgv1d .icon-shape .label{text-anchor:middle;}#mermaid-svg-6v3HYJS3AWeMgv1d .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6v3HYJS3AWeMgv1d .rough-node .label,#mermaid-svg-6v3HYJS3AWeMgv1d .node .label,#mermaid-svg-6v3HYJS3AWeMgv1d .image-shape .label,#mermaid-svg-6v3HYJS3AWeMgv1d .icon-shape .label{text-align:center;}#mermaid-svg-6v3HYJS3AWeMgv1d .node.clickable{cursor:pointer;}#mermaid-svg-6v3HYJS3AWeMgv1d .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6v3HYJS3AWeMgv1d .arrowheadPath{fill:#333333;}#mermaid-svg-6v3HYJS3AWeMgv1d .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6v3HYJS3AWeMgv1d .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6v3HYJS3AWeMgv1d .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6v3HYJS3AWeMgv1d .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6v3HYJS3AWeMgv1d .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6v3HYJS3AWeMgv1d .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6v3HYJS3AWeMgv1d .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6v3HYJS3AWeMgv1d .cluster text{fill:#333;}#mermaid-svg-6v3HYJS3AWeMgv1d .cluster span{color:#333;}#mermaid-svg-6v3HYJS3AWeMgv1d div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6v3HYJS3AWeMgv1d .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6v3HYJS3AWeMgv1d rect.text{fill:none;stroke-width:0;}#mermaid-svg-6v3HYJS3AWeMgv1d .icon-shape,#mermaid-svg-6v3HYJS3AWeMgv1d .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6v3HYJS3AWeMgv1d .icon-shape p,#mermaid-svg-6v3HYJS3AWeMgv1d .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6v3HYJS3AWeMgv1d .icon-shape .label rect,#mermaid-svg-6v3HYJS3AWeMgv1d .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6v3HYJS3AWeMgv1d .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6v3HYJS3AWeMgv1d .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6v3HYJS3AWeMgv1d :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Raft 组:3 节点,挂 1 个还能活
AppendEntrieslog[x=1]
AppendEntries
过半 2/3 即提交再异步通知
客户端写 x=1
Leader写本地 log任期 term=5
Follower 1ack
Follower 2ack
状态机应用
Raft 和 2PC 长得很像(都是两阶段),但灵魂不同:Raft 的多数派 + Leader 选举解决了 2PC 的单点阻塞,代价是只保证单组日志有序,不管跨分片的原子性。跨分片还得 2PC 来凑——这就是 Spanner 干的事。
5.2 Spanner:用原子钟把分布式变成单机
Google Spanner 的思路霸道:既然分布式缺全局时钟,我直接上硬件——每个数据中心配 GPS + 原子钟,造出 TrueTime(一个时间区间 [earliest, latest],保证真实时间一定落在里面)。事务提交时拿个时间戳,Committer 等到 latest 过去了再对外可见,就保证了全局有序。跨分片用 2PC,每分片内部用 Paxos 复制。
效果是:对外暴露的就是单机语义的 SERIALIZABLE,程序员不用写补偿。代价是:你得有 Google 的机房和原子钟,而且写延迟里有个 commit wait(一般几毫秒到十几毫秒)。国内 TiDB、OceanBase 学的是它的软件版:用 TSO(中心授时)代替原子钟,PD/时钟服务发全局递增时间戳,原理相通,精度和成本做了折中。
5.3 Percolator 与 Calvin:两种截然不同的原生路线
- Percolator(TiDB 用的就是它,源自 Google 处理海量网页的事务层):BigTable 上叠加的分布式事务。核心是两阶段 + 全局 TSO + 主锁(primary lock)+ 悬挂清理(把锁带上超时,别的事务能帮忙滚掉超时锁)。读用快照读,写用两阶段提交。好处是对业务透明,坏处是跨节点 2PC 延迟高,冲突重试多。
- Calvin( deterministic 确定性调度):换个脑回路——执行前先把一批事务按全局顺序排好队,所有副本按同一顺序跑,就不需要运行时加锁和两阶段了。适合高争用、逻辑可预判的场景(如抢票、支付),但要求事务逻辑事先可知,不支持交互式事务和非确定性操作。
#mermaid-svg-w5zewF0nDKD57RS0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-w5zewF0nDKD57RS0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-w5zewF0nDKD57RS0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-w5zewF0nDKD57RS0 .error-icon{fill:#552222;}#mermaid-svg-w5zewF0nDKD57RS0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-w5zewF0nDKD57RS0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-w5zewF0nDKD57RS0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-w5zewF0nDKD57RS0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-w5zewF0nDKD57RS0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-w5zewF0nDKD57RS0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-w5zewF0nDKD57RS0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-w5zewF0nDKD57RS0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-w5zewF0nDKD57RS0 .marker.cross{stroke:#333333;}#mermaid-svg-w5zewF0nDKD57RS0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-w5zewF0nDKD57RS0 p{margin:0;}#mermaid-svg-w5zewF0nDKD57RS0 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-w5zewF0nDKD57RS0 .cluster-label text{fill:#333;}#mermaid-svg-w5zewF0nDKD57RS0 .cluster-label span{color:#333;}#mermaid-svg-w5zewF0nDKD57RS0 .cluster-label span p{background-color:transparent;}#mermaid-svg-w5zewF0nDKD57RS0 .label text,#mermaid-svg-w5zewF0nDKD57RS0 span{fill:#333;color:#333;}#mermaid-svg-w5zewF0nDKD57RS0 .node rect,#mermaid-svg-w5zewF0nDKD57RS0 .node circle,#mermaid-svg-w5zewF0nDKD57RS0 .node ellipse,#mermaid-svg-w5zewF0nDKD57RS0 .node polygon,#mermaid-svg-w5zewF0nDKD57RS0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-w5zewF0nDKD57RS0 .rough-node .label text,#mermaid-svg-w5zewF0nDKD57RS0 .node .label text,#mermaid-svg-w5zewF0nDKD57RS0 .image-shape .label,#mermaid-svg-w5zewF0nDKD57RS0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-w5zewF0nDKD57RS0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-w5zewF0nDKD57RS0 .rough-node .label,#mermaid-svg-w5zewF0nDKD57RS0 .node .label,#mermaid-svg-w5zewF0nDKD57RS0 .image-shape .label,#mermaid-svg-w5zewF0nDKD57RS0 .icon-shape .label{text-align:center;}#mermaid-svg-w5zewF0nDKD57RS0 .node.clickable{cursor:pointer;}#mermaid-svg-w5zewF0nDKD57RS0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-w5zewF0nDKD57RS0 .arrowheadPath{fill:#333333;}#mermaid-svg-w5zewF0nDKD57RS0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-w5zewF0nDKD57RS0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-w5zewF0nDKD57RS0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-w5zewF0nDKD57RS0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-w5zewF0nDKD57RS0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-w5zewF0nDKD57RS0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-w5zewF0nDKD57RS0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-w5zewF0nDKD57RS0 .cluster text{fill:#333;}#mermaid-svg-w5zewF0nDKD57RS0 .cluster span{color:#333;}#mermaid-svg-w5zewF0nDKD57RS0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-w5zewF0nDKD57RS0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-w5zewF0nDKD57RS0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-w5zewF0nDKD57RS0 .icon-shape,#mermaid-svg-w5zewF0nDKD57RS0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-w5zewF0nDKD57RS0 .icon-shape p,#mermaid-svg-w5zewF0nDKD57RS0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-w5zewF0nDKD57RS0 .icon-shape .label rect,#mermaid-svg-w5zewF0nDKD57RS0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-w5zewF0nDKD57RS0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-w5zewF0nDKD57RS0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-w5zewF0nDKD57RS0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Calvin:先排队,再执行
1. sequencing 层把事务排成全局有序 batch
2. 广播给所有副本
3. 各副本按同一顺序确定性重放,无锁
Percolator:先拿号,再两阶段
1. 拿 start_ts快照读
2. 预写:写 data + 写锁选一行当 primary
3. 拿 commit_ts提交 primary
4. 异步提交 secondary挂了别人可帮忙清锁
小结:如果你能换库(TiDB/OceanBase/CockroachDB),优先用数据库原生分布式事务,业务代码几乎不用改。这是性价比最高的“强一致”,前提是业务能接受那几毫秒到几十毫秒的提交延迟和特定的部署成本。
6. 柔性派登场:TCC、Saga 与消息最终一致
互联网主流其实是柔性派:承认网络会出问题,不追求“此刻一致”,追求“最终账平 + 可查 + 可补”。三剑客是 TCC、Saga、消息事务,挨个拆。
6.1 TCC:把业务拆成 Try-Confirm-Cancel
TCC 是 2PC 的业务版:把每个本地事务拆成三个幂等方法。Try 做资源预留(冻结),Confirm 做真正扣减,Cancel 做释放。协调者先调所有 Try,全成调 Confirm,有一个失败调 Cancel。
经典例子:A 给 B 转 100。Try 阶段冻结 A 的 100(frozen += 100,可用余额减少但还没扣),Confirm 阶段真正扣掉冻结,Cancel 阶段解冻。
B 账户服务A 账户服务事务协调者B 账户服务A 账户服务事务协调者#mermaid-svg-xuivBvwlTnG83Jpy{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xuivBvwlTnG83Jpy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xuivBvwlTnG83Jpy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xuivBvwlTnG83Jpy .error-icon{fill:#552222;}#mermaid-svg-xuivBvwlTnG83Jpy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xuivBvwlTnG83Jpy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xuivBvwlTnG83Jpy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xuivBvwlTnG83Jpy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xuivBvwlTnG83Jpy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xuivBvwlTnG83Jpy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xuivBvwlTnG83Jpy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xuivBvwlTnG83Jpy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xuivBvwlTnG83Jpy .marker.cross{stroke:#333333;}#mermaid-svg-xuivBvwlTnG83Jpy svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xuivBvwlTnG83Jpy p{margin:0;}#mermaid-svg-xuivBvwlTnG83Jpy .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xuivBvwlTnG83Jpy text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-xuivBvwlTnG83Jpy .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-xuivBvwlTnG83Jpy .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-xuivBvwlTnG83Jpy .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-xuivBvwlTnG83Jpy .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-xuivBvwlTnG83Jpy #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-xuivBvwlTnG83Jpy .sequenceNumber{fill:white;}#mermaid-svg-xuivBvwlTnG83Jpy #sequencenumber{fill:#333;}#mermaid-svg-xuivBvwlTnG83Jpy #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-xuivBvwlTnG83Jpy .messageText{fill:#333;stroke:none;}#mermaid-svg-xuivBvwlTnG83Jpy .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xuivBvwlTnG83Jpy .labelText,#mermaid-svg-xuivBvwlTnG83Jpy .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-xuivBvwlTnG83Jpy .loopText,#mermaid-svg-xuivBvwlTnG83Jpy .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-xuivBvwlTnG83Jpy .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-xuivBvwlTnG83Jpy .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-xuivBvwlTnG83Jpy .noteText,#mermaid-svg-xuivBvwlTnG83Jpy .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-xuivBvwlTnG83Jpy .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xuivBvwlTnG83Jpy .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xuivBvwlTnG83Jpy .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-xuivBvwlTnG83Jpy .actorPopupMenu{position:absolute;}#mermaid-svg-xuivBvwlTnG83Jpy .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-xuivBvwlTnG83Jpy .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-xuivBvwlTnG83Jpy .actor-man circle,#mermaid-svg-xuivBvwlTnG83Jpy line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-xuivBvwlTnG83Jpy :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}若任一 Try 失败,走 CancelTry:freeze(A,100)1Try:freeze_入账额度(B,100)2Try 成功3Try 成功4Confirm:deduct(A,100)5Confirm:add(B,100)6OK7OK8
TCC 的好与坏都很极端。好在:资源预留在业务层,锁的是业务冻结字段,不是数据库行锁,并发高;坏在:业务侵入极强,每个操作要写三个方法,还要处理三个大坑:
// TCC 防悬挂 + 幂等的骨架,Seata/HMily 都是这个套路
@Transactional
public void tryFreeze(String xid, String orderId, long amount) {
// 1. 查事务状态表,有 CANCEL 标记说明空回滚,直接拒绝
if (txLogDao.hasCancel(xid)) return;
// 2. 幂等:Try 已执行过直接返回
if (txLogDao.hasTry(xid)) return;
// 3. 业务:冻结
accountDao.freeze(orderId, amount);
txLogDao.insertTry(xid, orderId);
}
所以 TCC 适合:强一致要求高、并发高、能接受代码改造的核心资金链路(银行、支付)。一般的 CRUD 业务用 TCC 就是大炮打蚊子。
6.2 Saga:长事务拆步,每步配补偿
Saga 是 1987 年的老论文,微服务时代翻红。思想:把长事务拆成 T1→T2→…→Tn,每步都是本地事务,每步配一个补偿动作 C1…Cn。任何一步失败,就倒着执行已成功步骤的补偿。
分两种写法:
- 协同式(Choreography):没中央协调者,每步做完发事件,下一步监听事件触发。去中心化,但流程散在各处,查问题要翻遍所有服务的日志。
- 编排式(Orchestration):有个 Saga 执行器( orchestrator)拿着流程定义一步步调,失败了按记录倒着补偿。可观测、可重放,生产推荐这个。
#mermaid-svg-YPjkPwUbWWAaMPuL{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YPjkPwUbWWAaMPuL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YPjkPwUbWWAaMPuL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YPjkPwUbWWAaMPuL .error-icon{fill:#552222;}#mermaid-svg-YPjkPwUbWWAaMPuL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YPjkPwUbWWAaMPuL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YPjkPwUbWWAaMPuL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YPjkPwUbWWAaMPuL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YPjkPwUbWWAaMPuL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YPjkPwUbWWAaMPuL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YPjkPwUbWWAaMPuL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YPjkPwUbWWAaMPuL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YPjkPwUbWWAaMPuL .marker.cross{stroke:#333333;}#mermaid-svg-YPjkPwUbWWAaMPuL svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YPjkPwUbWWAaMPuL p{margin:0;}#mermaid-svg-YPjkPwUbWWAaMPuL .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-YPjkPwUbWWAaMPuL .cluster-label text{fill:#333;}#mermaid-svg-YPjkPwUbWWAaMPuL .cluster-label span{color:#333;}#mermaid-svg-YPjkPwUbWWAaMPuL .cluster-label span p{background-color:transparent;}#mermaid-svg-YPjkPwUbWWAaMPuL .label text,#mermaid-svg-YPjkPwUbWWAaMPuL span{fill:#333;color:#333;}#mermaid-svg-YPjkPwUbWWAaMPuL .node rect,#mermaid-svg-YPjkPwUbWWAaMPuL .node circle,#mermaid-svg-YPjkPwUbWWAaMPuL .node ellipse,#mermaid-svg-YPjkPwUbWWAaMPuL .node polygon,#mermaid-svg-YPjkPwUbWWAaMPuL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YPjkPwUbWWAaMPuL .rough-node .label text,#mermaid-svg-YPjkPwUbWWAaMPuL .node .label text,#mermaid-svg-YPjkPwUbWWAaMPuL .image-shape .label,#mermaid-svg-YPjkPwUbWWAaMPuL .icon-shape .label{text-anchor:middle;}#mermaid-svg-YPjkPwUbWWAaMPuL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YPjkPwUbWWAaMPuL .rough-node .label,#mermaid-svg-YPjkPwUbWWAaMPuL .node .label,#mermaid-svg-YPjkPwUbWWAaMPuL .image-shape .label,#mermaid-svg-YPjkPwUbWWAaMPuL .icon-shape .label{text-align:center;}#mermaid-svg-YPjkPwUbWWAaMPuL .node.clickable{cursor:pointer;}#mermaid-svg-YPjkPwUbWWAaMPuL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YPjkPwUbWWAaMPuL .arrowheadPath{fill:#333333;}#mermaid-svg-YPjkPwUbWWAaMPuL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YPjkPwUbWWAaMPuL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YPjkPwUbWWAaMPuL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YPjkPwUbWWAaMPuL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YPjkPwUbWWAaMPuL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YPjkPwUbWWAaMPuL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YPjkPwUbWWAaMPuL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YPjkPwUbWWAaMPuL .cluster text{fill:#333;}#mermaid-svg-YPjkPwUbWWAaMPuL .cluster span{color:#333;}#mermaid-svg-YPjkPwUbWWAaMPuL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YPjkPwUbWWAaMPuL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YPjkPwUbWWAaMPuL rect.text{fill:none;stroke-width:0;}#mermaid-svg-YPjkPwUbWWAaMPuL .icon-shape,#mermaid-svg-YPjkPwUbWWAaMPuL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YPjkPwUbWWAaMPuL .icon-shape p,#mermaid-svg-YPjkPwUbWWAaMPuL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YPjkPwUbWWAaMPuL .icon-shape .label rect,#mermaid-svg-YPjkPwUbWWAaMPuL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YPjkPwUbWWAaMPuL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YPjkPwUbWWAaMPuL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YPjkPwUbWWAaMPuL :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
T3 失败:倒着补偿 C2 -> C1
T3 失败
C2:库存加回去
C1:订单置取消
Saga 正常:T1 -> T2 -> T3
T1:创订单C1:取消订单
T2:扣库存C2:回补库存
T3:扣款C3:退款
Saga 和 TCC 的区别一句话:TCC 是“先冻结、再实锤”,Saga 是“直接干、错了再赔”。Saga 没有预留阶段,中间状态是对外可见的(订单建了、库存扣了,钱还没扣),所以 Saga 不适合强隔离场景。它的补偿也不是回滚,而是“业务逆操作”,语义上可能不等价(退款是再转一笔,不是把之前那笔抹掉)。
Saga 状态机长这样,写框架的人必须吃透:
#mermaid-svg-JbtUGAeGQqo0HUj6{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JbtUGAeGQqo0HUj6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JbtUGAeGQqo0HUj6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JbtUGAeGQqo0HUj6 .error-icon{fill:#552222;}#mermaid-svg-JbtUGAeGQqo0HUj6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JbtUGAeGQqo0HUj6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JbtUGAeGQqo0HUj6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JbtUGAeGQqo0HUj6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JbtUGAeGQqo0HUj6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JbtUGAeGQqo0HUj6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JbtUGAeGQqo0HUj6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JbtUGAeGQqo0HUj6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JbtUGAeGQqo0HUj6 .marker.cross{stroke:#333333;}#mermaid-svg-JbtUGAeGQqo0HUj6 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JbtUGAeGQqo0HUj6 p{margin:0;}#mermaid-svg-JbtUGAeGQqo0HUj6 defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-JbtUGAeGQqo0HUj6 g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-JbtUGAeGQqo0HUj6 g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-JbtUGAeGQqo0HUj6 g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-JbtUGAeGQqo0HUj6 g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-JbtUGAeGQqo0HUj6 g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-JbtUGAeGQqo0HUj6 .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-JbtUGAeGQqo0HUj6 .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-JbtUGAeGQqo0HUj6 .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-JbtUGAeGQqo0HUj6 .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-JbtUGAeGQqo0HUj6 .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-JbtUGAeGQqo0HUj6 .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-JbtUGAeGQqo0HUj6 .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-JbtUGAeGQqo0HUj6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JbtUGAeGQqo0HUj6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JbtUGAeGQqo0HUj6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JbtUGAeGQqo0HUj6 .edgeLabel .label text{fill:#333;}#mermaid-svg-JbtUGAeGQqo0HUj6 .label div .edgeLabel{color:#333;}#mermaid-svg-JbtUGAeGQqo0HUj6 .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-JbtUGAeGQqo0HUj6 .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-JbtUGAeGQqo0HUj6 .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-JbtUGAeGQqo0HUj6 .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-JbtUGAeGQqo0HUj6 .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-JbtUGAeGQqo0HUj6 .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JbtUGAeGQqo0HUj6 .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JbtUGAeGQqo0HUj6 #statediagram-barbEnd{fill:#333333;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JbtUGAeGQqo0HUj6 .cluster-label,#mermaid-svg-JbtUGAeGQqo0HUj6 .nodeLabel{color:#131300;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-JbtUGAeGQqo0HUj6 .note-edge{stroke-dasharray:5;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-note text{fill:black;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram-note .nodeLabel{color:black;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagram .edgeLabel{color:red;}#mermaid-svg-JbtUGAeGQqo0HUj6 #dependencyStart,#mermaid-svg-JbtUGAeGQqo0HUj6 #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-JbtUGAeGQqo0HUj6 .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JbtUGAeGQqo0HUj6 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
开始 Saga
执行正向 T_i
T_i 成功,记日志
T_i 失败
倒序执行 C_j每个补偿可重试
补偿完,进终态
补偿也失败记死信,人工介入
全部 T 成功
Running
Forward
Compensating
Compensated
Failed_Need_Manual
Succeed
6.3 可靠消息最终一致:本地消息表、Outbox 与 MQ 事务消息
这是国内用得最多的路线,核心矛盾只有一个:本地数据库提交和 MQ 发送是两个系统,做不到原子。先发消息再提交,库挂了消息发出去了;先提交再发消息,刚提交完机器挂了消息没发。怎么办?答案是把“发消息”也变成数据库的一部分。
路线 A:本地消息表(早期做法)
业务表和消息表在同一个库、同一个本地事务里一起写。后台线程扫消息表发 MQ,收到 ack 删消息。简单粗暴,但要业务侵入(每个业务都要写消息表),还要处理轮询压力。
路线 B:Outbox + CDC(现在的主流)
Outbox 就是把本地消息表标准化:业务只管写业务表 + outbox 表(同一事务),Debezium/Canal 监听 binlog 把 outbox 行转成 MQ 消息,下游消费。业务代码干净了,轮询压力也没了,靠 binlog 的有序性保证消息有序。开销是多运一套 CDC 组件,还要处理 CDC 延迟和重复投递。
路线 C:RocketMQ 事务消息(Half Message)
RocketMQ 把 Outbox 做进了 broker:先发 half 消息(对消费者不可见),执行本地事务,本地成了发 commit 让消息可见,挂了发 rollback 删掉。broker 还会反查长时间没决议的 half 消息,问生产者“你到底成了没”。时序如下:
本地库消费者库存/记账服务Broker生产者订单服务本地库消费者库存/记账服务Broker生产者订单服务#mermaid-svg-ejstt7U8bAeKGF9Y{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ejstt7U8bAeKGF9Y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ejstt7U8bAeKGF9Y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ejstt7U8bAeKGF9Y .error-icon{fill:#552222;}#mermaid-svg-ejstt7U8bAeKGF9Y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ejstt7U8bAeKGF9Y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ejstt7U8bAeKGF9Y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ejstt7U8bAeKGF9Y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ejstt7U8bAeKGF9Y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ejstt7U8bAeKGF9Y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ejstt7U8bAeKGF9Y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ejstt7U8bAeKGF9Y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ejstt7U8bAeKGF9Y .marker.cross{stroke:#333333;}#mermaid-svg-ejstt7U8bAeKGF9Y svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ejstt7U8bAeKGF9Y p{margin:0;}#mermaid-svg-ejstt7U8bAeKGF9Y .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ejstt7U8bAeKGF9Y text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ejstt7U8bAeKGF9Y .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ejstt7U8bAeKGF9Y .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ejstt7U8bAeKGF9Y .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ejstt7U8bAeKGF9Y .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ejstt7U8bAeKGF9Y #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ejstt7U8bAeKGF9Y .sequenceNumber{fill:white;}#mermaid-svg-ejstt7U8bAeKGF9Y #sequencenumber{fill:#333;}#mermaid-svg-ejstt7U8bAeKGF9Y #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ejstt7U8bAeKGF9Y .messageText{fill:#333;stroke:none;}#mermaid-svg-ejstt7U8bAeKGF9Y .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ejstt7U8bAeKGF9Y .labelText,#mermaid-svg-ejstt7U8bAeKGF9Y .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ejstt7U8bAeKGF9Y .loopText,#mermaid-svg-ejstt7U8bAeKGF9Y .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ejstt7U8bAeKGF9Y .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ejstt7U8bAeKGF9Y .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ejstt7U8bAeKGF9Y .noteText,#mermaid-svg-ejstt7U8bAeKGF9Y .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ejstt7U8bAeKGF9Y .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ejstt7U8bAeKGF9Y .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ejstt7U8bAeKGF9Y .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ejstt7U8bAeKGF9Y .actorPopupMenu{position:absolute;}#mermaid-svg-ejstt7U8bAeKGF9Y .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ejstt7U8bAeKGF9Y .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ejstt7U8bAeKGF9Y .actor-man circle,#mermaid-svg-ejstt7U8bAeKGF9Y line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ejstt7U8bAeKGF9Y :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}alt[本地成功][本地失败][生产者挂了没决议]发 Half 消息(不可见)1Half 存好2执行本地事务创订单 + 写状态3commit(让消息可见)4投递消息5执行本地事务6ACK7rollback(删 half)8反查:到底提交吗?9查本地事务状态表后答复10
三者怎么选?新项目直接 Outbox+CDC,生态成熟、可观测;已深度绑定 RocketMQ 且链路简单,用事务消息最省事;存量老项目改不动,本地消息表打个补丁最快。但都要记住:消息最终一致只保证“最终送到”,不保证“不重复”、不保证“有序”、不保证“下游一定成功”,幂等消费、顺序键、死信队列一个都不能少。
7. 整体架构:一个典型的柔性事务平台长什么样
把上面的零件拼成生产可用的样子,一般是四层。拿 Seata + Outbox + MQ 的混合架构举例,这是国内中大厂最常见的拼法:核心资金走 Seata AT/TCC,跨域通知走消息最终一致。
#mermaid-svg-Mc4WJxSsQeaiF9L8{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Mc4WJxSsQeaiF9L8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .error-icon{fill:#552222;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .marker.cross{stroke:#333333;}#mermaid-svg-Mc4WJxSsQeaiF9L8 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Mc4WJxSsQeaiF9L8 p{margin:0;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .cluster-label text{fill:#333;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .cluster-label span{color:#333;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .cluster-label span p{background-color:transparent;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .label text,#mermaid-svg-Mc4WJxSsQeaiF9L8 span{fill:#333;color:#333;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .node rect,#mermaid-svg-Mc4WJxSsQeaiF9L8 .node circle,#mermaid-svg-Mc4WJxSsQeaiF9L8 .node ellipse,#mermaid-svg-Mc4WJxSsQeaiF9L8 .node polygon,#mermaid-svg-Mc4WJxSsQeaiF9L8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .rough-node .label text,#mermaid-svg-Mc4WJxSsQeaiF9L8 .node .label text,#mermaid-svg-Mc4WJxSsQeaiF9L8 .image-shape .label,#mermaid-svg-Mc4WJxSsQeaiF9L8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .rough-node .label,#mermaid-svg-Mc4WJxSsQeaiF9L8 .node .label,#mermaid-svg-Mc4WJxSsQeaiF9L8 .image-shape .label,#mermaid-svg-Mc4WJxSsQeaiF9L8 .icon-shape .label{text-align:center;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .node.clickable{cursor:pointer;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .arrowheadPath{fill:#333333;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Mc4WJxSsQeaiF9L8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Mc4WJxSsQeaiF9L8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Mc4WJxSsQeaiF9L8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .cluster text{fill:#333;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .cluster span{color:#333;}#mermaid-svg-Mc4WJxSsQeaiF9L8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Mc4WJxSsQeaiF9L8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .icon-shape,#mermaid-svg-Mc4WJxSsQeaiF9L8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .icon-shape p,#mermaid-svg-Mc4WJxSsQeaiF9L8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .icon-shape .label rect,#mermaid-svg-Mc4WJxSsQeaiF9L8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Mc4WJxSsQeaiF9L8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Mc4WJxSsQeaiF9L8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Mc4WJxSsQeaiF9L8 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
存储与中间件
事务协调层
业务应用层
1. 申请 XID
2. RPC 带 XID
2. RPC 带 XID
查幂等
commit/rollback 决议
决议
网关 / App
order-serviceTM 事务发起方
account-serviceRM
stock-serviceRM
Seata ServerTC:管全局 XID分支注册 / 决议
Saga 执行器状态机 + 补偿调度
RocketMQ BrokerHalf 消息 + 反查
order-db业务表 + undo_log+ tx_state
account-db业务表 + undo_log
stock-db
outbox 表binlog
Debezium / Canalbinlog -> MQ
Redis幂等键 / 去重
数据流走一遍“下单扣库存扣余额”:
#mermaid-svg-HlrK8rd0mnCmbInW{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HlrK8rd0mnCmbInW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HlrK8rd0mnCmbInW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HlrK8rd0mnCmbInW .error-icon{fill:#552222;}#mermaid-svg-HlrK8rd0mnCmbInW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HlrK8rd0mnCmbInW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HlrK8rd0mnCmbInW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HlrK8rd0mnCmbInW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HlrK8rd0mnCmbInW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HlrK8rd0mnCmbInW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HlrK8rd0mnCmbInW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HlrK8rd0mnCmbInW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HlrK8rd0mnCmbInW .marker.cross{stroke:#333333;}#mermaid-svg-HlrK8rd0mnCmbInW svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HlrK8rd0mnCmbInW p{margin:0;}#mermaid-svg-HlrK8rd0mnCmbInW .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-HlrK8rd0mnCmbInW .cluster-label text{fill:#333;}#mermaid-svg-HlrK8rd0mnCmbInW .cluster-label span{color:#333;}#mermaid-svg-HlrK8rd0mnCmbInW .cluster-label span p{background-color:transparent;}#mermaid-svg-HlrK8rd0mnCmbInW .label text,#mermaid-svg-HlrK8rd0mnCmbInW span{fill:#333;color:#333;}#mermaid-svg-HlrK8rd0mnCmbInW .node rect,#mermaid-svg-HlrK8rd0mnCmbInW .node circle,#mermaid-svg-HlrK8rd0mnCmbInW .node ellipse,#mermaid-svg-HlrK8rd0mnCmbInW .node polygon,#mermaid-svg-HlrK8rd0mnCmbInW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HlrK8rd0mnCmbInW .rough-node .label text,#mermaid-svg-HlrK8rd0mnCmbInW .node .label text,#mermaid-svg-HlrK8rd0mnCmbInW .image-shape .label,#mermaid-svg-HlrK8rd0mnCmbInW .icon-shape .label{text-anchor:middle;}#mermaid-svg-HlrK8rd0mnCmbInW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HlrK8rd0mnCmbInW .rough-node .label,#mermaid-svg-HlrK8rd0mnCmbInW .node .label,#mermaid-svg-HlrK8rd0mnCmbInW .image-shape .label,#mermaid-svg-HlrK8rd0mnCmbInW .icon-shape .label{text-align:center;}#mermaid-svg-HlrK8rd0mnCmbInW .node.clickable{cursor:pointer;}#mermaid-svg-HlrK8rd0mnCmbInW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HlrK8rd0mnCmbInW .arrowheadPath{fill:#333333;}#mermaid-svg-HlrK8rd0mnCmbInW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HlrK8rd0mnCmbInW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HlrK8rd0mnCmbInW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HlrK8rd0mnCmbInW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HlrK8rd0mnCmbInW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HlrK8rd0mnCmbInW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HlrK8rd0mnCmbInW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HlrK8rd0mnCmbInW .cluster text{fill:#333;}#mermaid-svg-HlrK8rd0mnCmbInW .cluster span{color:#333;}#mermaid-svg-HlrK8rd0mnCmbInW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HlrK8rd0mnCmbInW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HlrK8rd0mnCmbInW rect.text{fill:none;stroke-width:0;}#mermaid-svg-HlrK8rd0mnCmbInW .icon-shape,#mermaid-svg-HlrK8rd0mnCmbInW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HlrK8rd0mnCmbInW .icon-shape p,#mermaid-svg-HlrK8rd0mnCmbInW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HlrK8rd0mnCmbInW .icon-shape .label rect,#mermaid-svg-HlrK8rd0mnCmbInW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HlrK8rd0mnCmbInW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HlrK8rd0mnCmbInW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HlrK8rd0mnCmbInW :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
用户点下单
order-service:创建全局 XID写订单 TRYING
调 account Try:冻结 100
调 stock Try:冻结 1 件
都 Try 成功?
TC 发 Confirm实扣 + 订单置 PAID写 outbox 行
TC 发 Cancel解冻 + 订单置 CANCELED
CDC 抓 outbox binlog发 MQ:order_paid
下游积分/仓储幂等消费失败进重试/死信
这张数据流图里藏了三个生产要点:
8. ER 关系:事务表到底怎么建
光讲协议不落地表结构,等于没讲。这里给一套可直接抄的表设计:订单 + 账户 + 全局事务三张核心,外加 Seata AT 的 undo_log 和 Saga 的状态表。MySQL 8 语法。
#mermaid-svg-ztlpsdYmlIs0wdSx{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ztlpsdYmlIs0wdSx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ztlpsdYmlIs0wdSx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ztlpsdYmlIs0wdSx .error-icon{fill:#552222;}#mermaid-svg-ztlpsdYmlIs0wdSx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ztlpsdYmlIs0wdSx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ztlpsdYmlIs0wdSx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ztlpsdYmlIs0wdSx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ztlpsdYmlIs0wdSx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ztlpsdYmlIs0wdSx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ztlpsdYmlIs0wdSx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ztlpsdYmlIs0wdSx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ztlpsdYmlIs0wdSx .marker.cross{stroke:#333333;}#mermaid-svg-ztlpsdYmlIs0wdSx svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ztlpsdYmlIs0wdSx p{margin:0;}#mermaid-svg-ztlpsdYmlIs0wdSx .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-ztlpsdYmlIs0wdSx .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-ztlpsdYmlIs0wdSx .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-ztlpsdYmlIs0wdSx .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-ztlpsdYmlIs0wdSx .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-ztlpsdYmlIs0wdSx .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ztlpsdYmlIs0wdSx .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-ztlpsdYmlIs0wdSx .node rect,#mermaid-svg-ztlpsdYmlIs0wdSx .node circle,#mermaid-svg-ztlpsdYmlIs0wdSx .node ellipse,#mermaid-svg-ztlpsdYmlIs0wdSx .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ztlpsdYmlIs0wdSx .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-ztlpsdYmlIs0wdSx .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ztlpsdYmlIs0wdSx .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ztlpsdYmlIs0wdSx .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-ztlpsdYmlIs0wdSx .edgeLabel .label text{fill:#333;}#mermaid-svg-ztlpsdYmlIs0wdSx :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
1:N 一个全局含多分支
1:1 幂等/防悬挂
订单绑定 XID
1:N 冻结明细
1:N 业务完成后写 outbox
GLOBAL_TX
varchar
xid
PK
全局事务ID
varchar
status
BEGIN/COMMITTING/ROLLBACKING/COMMITTED/ROLLBACKED
datetime
gmt_create
datetime
gmt_modified
varchar
app_name
发起方
BRANCH_TX
bigint
id
PK
varchar
xid
FK
关联全局
varchar
branch_id
分支ID
varchar
resource_id
库/服务标识
varchar
status
PHASE1_DONE/PHASE2_DONE
varchar
undo_info
回滚上下文
TX_STATE
varchar
xid
PK
varchar
branch_key
业务幂等键 orderId
varchar
state
TRY_DONE/CONFIRM_DONE/CANCEL_DONE
datetime
gmt_create
ORDER_INFO
varchar
order_id
PK
varchar
xid
关联全局事务
varchar
status
TRYING/PAID/CANCELED
decimal
amount
datetime
gmt_create
ACCOUNT
bigint
user_id
PK
decimal
balance
可用
decimal
frozen
冻结
int
version
乐观锁
ACCOUNT_FREEZE
bigint
id
PK
bigint
user_id
FK
varchar
order_id
幂等键
decimal
amount
varchar
status
FROZEN/DEDUCTED/RELEASED
OUTBOX
bigint
id
PK
varchar
aggregate_type
order
varchar
aggregate_id
orderId
varchar
event_type
order_paid
json
payload
varchar
status
NEW/SENT
建表 SQL(精简可跑版):
— 全局事务表(Seata server 存的动作,业务侧可自建轻量版用于对账)
CREATE TABLE global_tx (
xid VARCHAR(128) PRIMARY KEY,
status VARCHAR(32) NOT NULL,
app_name VARCHAR(64),
gmt_create DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
gmt_modified DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_status_time (status, gmt_modified)
) ENGINE=InnoDB;
— 分支与幂等状态(防悬挂+空回滚的核心)
CREATE TABLE tx_state (
xid VARCHAR(128) NOT NULL,
branch_key VARCHAR(128) NOT NULL, — 如 order_id,防重
state VARCHAR(32) NOT NULL, — TRY_DONE / CONFIRM_DONE / CANCEL_DONE
gmt_create DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (xid, branch_key)
) ENGINE=InnoDB;
— 账户:余额与冻结分离,TCC 必备
CREATE TABLE account (
user_id BIGINT PRIMARY KEY,
balance DECIMAL(18,2) NOT NULL DEFAULT 0,
frozen DECIMAL(18,2) NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 0
) ENGINE=InnoDB;
CREATE TABLE account_freeze (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_id VARCHAR(64) NOT NULL,
amount DECIMAL(18,2) NOT NULL,
status VARCHAR(16) NOT NULL, — FROZEN / DEDUCTED / RELEASED
UNIQUE KEY uk_order (order_id),
KEY idx_user (user_id)
) ENGINE=InnoDB;
— Seata AT 的 undo_log(框架自动写,贴出来让你知道它长啥样)
CREATE TABLE undo_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
context VARCHAR(128),
rollback_info LONGBLOB NOT NULL, — 存前后镜像,JSON+压缩
log_status INT NOT NULL, — 0 normal,1 globally finished
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE=InnoDB;
— Outbox 表
CREATE TABLE outbox (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
aggregate_type VARCHAR(64) NOT NULL,
aggregate_id VARCHAR(128) NOT NULL,
event_type VARCHAR(64) NOT NULL,
payload JSON NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'NEW',
gmt_create DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_status_id (status, id)
) ENGINE=InnoDB;
三句叮嘱,都是线上踩出来的:
- tx_state 的 (xid, branch_key) 必须唯一索引,这是幂等的物理兜底,应用层 if exists return 挡不住并发,唯一键才能挡住。
- account 一定要 balance 与 frozen 分列,别用一个字段加减。混在一起,并发一高对账能把人看哭。
- outbox 按 (status, id) 建索引,CDC/轮询扫 WHERE status='NEW' ORDER BY id LIMIT 500 才能走索引,不然全表扫把业务库拖死。
9. Seata 全家桶:AT、TCC、Saga、XA 怎么选
Seata 是国内分布式事务的事实标准,理解它就理解了大半个工业界。它的角色对标 XA:TC(Transaction Coordinator,独立的 seata-server)、TM(发起方)、RM(参与方)。一句话:Seata 把 2PC 的骨架留下来,把锁和回滚的实现换成了更适合微服务的样子。
#mermaid-svg-TKiUUhxAgIW7Fokw{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TKiUUhxAgIW7Fokw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TKiUUhxAgIW7Fokw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TKiUUhxAgIW7Fokw .error-icon{fill:#552222;}#mermaid-svg-TKiUUhxAgIW7Fokw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TKiUUhxAgIW7Fokw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TKiUUhxAgIW7Fokw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TKiUUhxAgIW7Fokw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TKiUUhxAgIW7Fokw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TKiUUhxAgIW7Fokw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TKiUUhxAgIW7Fokw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TKiUUhxAgIW7Fokw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TKiUUhxAgIW7Fokw .marker.cross{stroke:#333333;}#mermaid-svg-TKiUUhxAgIW7Fokw svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TKiUUhxAgIW7Fokw p{margin:0;}#mermaid-svg-TKiUUhxAgIW7Fokw .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-TKiUUhxAgIW7Fokw .cluster-label text{fill:#333;}#mermaid-svg-TKiUUhxAgIW7Fokw .cluster-label span{color:#333;}#mermaid-svg-TKiUUhxAgIW7Fokw .cluster-label span p{background-color:transparent;}#mermaid-svg-TKiUUhxAgIW7Fokw .label text,#mermaid-svg-TKiUUhxAgIW7Fokw span{fill:#333;color:#333;}#mermaid-svg-TKiUUhxAgIW7Fokw .node rect,#mermaid-svg-TKiUUhxAgIW7Fokw .node circle,#mermaid-svg-TKiUUhxAgIW7Fokw .node ellipse,#mermaid-svg-TKiUUhxAgIW7Fokw .node polygon,#mermaid-svg-TKiUUhxAgIW7Fokw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TKiUUhxAgIW7Fokw .rough-node .label text,#mermaid-svg-TKiUUhxAgIW7Fokw .node .label text,#mermaid-svg-TKiUUhxAgIW7Fokw .image-shape .label,#mermaid-svg-TKiUUhxAgIW7Fokw .icon-shape .label{text-anchor:middle;}#mermaid-svg-TKiUUhxAgIW7Fokw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TKiUUhxAgIW7Fokw .rough-node .label,#mermaid-svg-TKiUUhxAgIW7Fokw .node .label,#mermaid-svg-TKiUUhxAgIW7Fokw .image-shape .label,#mermaid-svg-TKiUUhxAgIW7Fokw .icon-shape .label{text-align:center;}#mermaid-svg-TKiUUhxAgIW7Fokw .node.clickable{cursor:pointer;}#mermaid-svg-TKiUUhxAgIW7Fokw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TKiUUhxAgIW7Fokw .arrowheadPath{fill:#333333;}#mermaid-svg-TKiUUhxAgIW7Fokw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TKiUUhxAgIW7Fokw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TKiUUhxAgIW7Fokw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TKiUUhxAgIW7Fokw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TKiUUhxAgIW7Fokw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TKiUUhxAgIW7Fokw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TKiUUhxAgIW7Fokw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TKiUUhxAgIW7Fokw .cluster text{fill:#333;}#mermaid-svg-TKiUUhxAgIW7Fokw .cluster span{color:#333;}#mermaid-svg-TKiUUhxAgIW7Fokw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TKiUUhxAgIW7Fokw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TKiUUhxAgIW7Fokw rect.text{fill:none;stroke-width:0;}#mermaid-svg-TKiUUhxAgIW7Fokw .icon-shape,#mermaid-svg-TKiUUhxAgIW7Fokw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TKiUUhxAgIW7Fokw .icon-shape p,#mermaid-svg-TKiUUhxAgIW7Fokw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TKiUUhxAgIW7Fokw .icon-shape .label rect,#mermaid-svg-TKiUUhxAgIW7Fokw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TKiUUhxAgIW7Fokw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TKiUUhxAgIW7Fokw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TKiUUhxAgIW7Fokw :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
1. 申请 XID
2. RPC 带 XID
3. RPC 带 XID
4. 注册分支
4. 注册分支
5. 一阶段本地提交(AT 直接提交+记 undo)
5. 一阶段
6. commit/rollback 决议
7. 异步二阶段
7. 异步二阶段
TM:order-service@GlobalTransactional
TC:seata-server记全局 + 分支
RM:account-service@TwoPhaseBusinessAction
RM:stock-service
account-db
stock-db
四种模式的本质区别:
| XA | 原生 XA prepare,不提交 | commit/rollback | 数据库行锁(持有最久) | 低(数据源代理) | 同机房、多库、存量改造 |
| AT | 解析 SQL 记前后镜像,直接提交本地事务 | commit 时删 undo,rollback 时用前镜像反向 SQL | 全局写锁(Seata server 侧逻辑锁,持有短) | 低(只加数据源代理+undo_log表) | 90% 的 CRUD 跨库,首选 |
| TCC | 手写 Try 冻结 | 手写 Confirm/Cancel | 业务冻结字段 | 高(三倍代码) | 资金、库存等高冲突核心链路 |
| Saga | 直接提交本地事务 | 失败时调补偿 | 不锁(裸奔+事后赔) | 中(写正向+补偿+状态机) | 长流程、多步骤、低隔离可接受 |
AT 值得单独展开,因为它最常用也最被误解。AT 一阶段直接提交了本地事务,二阶段回滚靠 undo_log 里存的前镜像拼反向 SQL。它解决脏写靠全局写隔离:提交前去 TC 查一下“我要写的行有没有别的全局事务没完事”,有就等。这是个逻辑锁,不是数据库锁,所以性能比 XA 好得多,但依然有死锁和长事务持有全局锁的风险。AT 默认是 READ UNCOMMITTED 语义——全局事务没提交,别的全局事务可能读到中间值。接受不了就上 TCC 或 XA。
// Seata AT:业务代码几乎零改,靠代理数据源 + 注解
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(Order order) {
orderDao.insert(order); // 库1:代理数据源自动记前后镜像
accountService.deduct(order.getUserId(), order.getAmount()); // RPC 带 XID
stockService.deduct(order.getSku(), 1); // RPC 带 XID
// 任一抛异常,框架自动全局回滚
}
// Seata TCC:三个方法 + 注解,框架只管调用顺序
@TwoPhaseBusinessAction(name = "freezeAction", commitMethod = "confirm", rollbackMethod = "cancel")
public boolean tryFreeze(BusinessActionContext ctx, String orderId, long amount) { ... }
public boolean confirm(BusinessActionContext ctx) { ... } // 幂等 + 判空
public boolean cancel(BusinessActionContext ctx) { ... } // 空回滚 + 防悬挂 + 幂等
10. 幂等、悬挂与重试:柔性事务的三条命
柔性方案把“做一次就对”变成了“做多次最终对”,所以重试 + 幂等 + 去重 不是优化项,是命。这一章是 checklist,上线前逐条过。
10.1 幂等的五个层级,从 low 到高
10.2 TCC 三坑的标准解(再强调一遍,因为必踩)
#mermaid-svg-wsOoReiser1M3eyy{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wsOoReiser1M3eyy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wsOoReiser1M3eyy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wsOoReiser1M3eyy .error-icon{fill:#552222;}#mermaid-svg-wsOoReiser1M3eyy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wsOoReiser1M3eyy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wsOoReiser1M3eyy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wsOoReiser1M3eyy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wsOoReiser1M3eyy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wsOoReiser1M3eyy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wsOoReiser1M3eyy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wsOoReiser1M3eyy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wsOoReiser1M3eyy .marker.cross{stroke:#333333;}#mermaid-svg-wsOoReiser1M3eyy svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wsOoReiser1M3eyy p{margin:0;}#mermaid-svg-wsOoReiser1M3eyy .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-wsOoReiser1M3eyy .cluster-label text{fill:#333;}#mermaid-svg-wsOoReiser1M3eyy .cluster-label span{color:#333;}#mermaid-svg-wsOoReiser1M3eyy .cluster-label span p{background-color:transparent;}#mermaid-svg-wsOoReiser1M3eyy .label text,#mermaid-svg-wsOoReiser1M3eyy span{fill:#333;color:#333;}#mermaid-svg-wsOoReiser1M3eyy .node rect,#mermaid-svg-wsOoReiser1M3eyy .node circle,#mermaid-svg-wsOoReiser1M3eyy .node ellipse,#mermaid-svg-wsOoReiser1M3eyy .node polygon,#mermaid-svg-wsOoReiser1M3eyy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wsOoReiser1M3eyy .rough-node .label text,#mermaid-svg-wsOoReiser1M3eyy .node .label text,#mermaid-svg-wsOoReiser1M3eyy .image-shape .label,#mermaid-svg-wsOoReiser1M3eyy .icon-shape .label{text-anchor:middle;}#mermaid-svg-wsOoReiser1M3eyy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wsOoReiser1M3eyy .rough-node .label,#mermaid-svg-wsOoReiser1M3eyy .node .label,#mermaid-svg-wsOoReiser1M3eyy .image-shape .label,#mermaid-svg-wsOoReiser1M3eyy .icon-shape .label{text-align:center;}#mermaid-svg-wsOoReiser1M3eyy .node.clickable{cursor:pointer;}#mermaid-svg-wsOoReiser1M3eyy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wsOoReiser1M3eyy .arrowheadPath{fill:#333333;}#mermaid-svg-wsOoReiser1M3eyy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wsOoReiser1M3eyy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wsOoReiser1M3eyy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wsOoReiser1M3eyy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wsOoReiser1M3eyy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wsOoReiser1M3eyy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wsOoReiser1M3eyy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wsOoReiser1M3eyy .cluster text{fill:#333;}#mermaid-svg-wsOoReiser1M3eyy .cluster span{color:#333;}#mermaid-svg-wsOoReiser1M3eyy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wsOoReiser1M3eyy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wsOoReiser1M3eyy rect.text{fill:none;stroke-width:0;}#mermaid-svg-wsOoReiser1M3eyy .icon-shape,#mermaid-svg-wsOoReiser1M3eyy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wsOoReiser1M3eyy .icon-shape p,#mermaid-svg-wsOoReiser1M3eyy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wsOoReiser1M3eyy .icon-shape .label rect,#mermaid-svg-wsOoReiser1M3eyy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wsOoReiser1M3eyy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wsOoReiser1M3eyy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wsOoReiser1M3eyy :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Confirm 已做
Cancel 已做 + 现在是 Try
Try 已做 + 现在是 Cancel
无记录 + 现在是 Cancel
无记录 + 现在是 Try
无记录 + 现在是 Confirm
收到 Try/Confirm/Cancelxid + orderId
查 tx_state有记录?
直接回成功幂等返回
拒绝执行防悬挂
正常走 Cancel解冻
记 CANCEL 空回滚直接回成功
执行业务 + 记 TRY
正常走 Confirm
口诀:Cancel 要记空,Try 要查空,Confirm/Cancel 要判重。Seata TCC 和 Himalaya/HMily 的 demo 里都有这段,别自己造轮子,抄成熟框架的就行。
10.3 重试与死信:重试要有节制,死信要有人看
- 重试用指数退避 + 抖动:1s、2s、4s、8s…加随机抖动,避免所有失败一起重试打爆下游(惊群)。
- 区分可重试与不可重试:超时、500、DB 死锁可重试;参数非法、余额不足、幂等冲突不可重试,直接进补偿/人工。
- MQ 消费失败超过 N 次进死信队列(DLQ),配告警 + 重投工具。死信没人看,等于把问题扫进地毯底下。
- 消费端永远先判幂等再干活,干完活再 ack。顺序反了(先 ack 后干活,挂了就丢消息),神仙难救。
11. 隔离在分布式下:全局锁、快照与死锁
单机靠锁 + MVCC,分布式靠什么?三条路:
分布式死锁检测是个脏活:单机 wait-for graph 全局不可见,只能靠超时。但超时误杀多,工业界折中是按资源排序加锁 + 短超时 + 快速重试。业务层能做的:所有服务按同一顺序拿资源(先 account 后 stock,全局统一),死锁概率直接降一个数量级。
快照隔离(SI)在分布式下还有个隐形坑:SI 不是可串行化,写偏斜照样发生。TiDB 默认 SI,要可串行化得开 tidb_txn_mode = 'pessimistic' 或显式 SELECT … FOR UPDATE。别以为换了分布式数据库就万事大吉,隔离级别还是要显式选。
12. 选型决策:没有银弹,只有权衡
把所有方案放在一张表里,选型时对着业务特征打勾。
| 一致性 | 强 | 接近强(读未提交) | 强(业务层) | 最终 | 最终 | 强 |
| 可用性(分区时) | 差(阻塞) | 中 | 中 | 高 | 高 | 高(多数派活着就行) |
| 性能/吞吐 | 低(锁久+2次RTT) | 中高 | 高(无DB锁) | 高 | 最高(异步) | 中(commit 有额外开销) |
| 业务侵入 | 低 | 低 | 高 | 中 | 低-中 | 极低 |
| 适用数据 | 关系库 | 关系库 | 任意(含RPC) | 任意 | 异步可接受 | 全托管新库 |
| 典型场景 | 同机房多库 | 跨库 CRUD | 钱、券、库存 | 长流程审批/旅行预订 | 通知、记账、同步 | 新业务、全球部署 |
决策树(照着走,八成不会错):
#mermaid-svg-AaLwaVklfIvhXqUi{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AaLwaVklfIvhXqUi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AaLwaVklfIvhXqUi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AaLwaVklfIvhXqUi .error-icon{fill:#552222;}#mermaid-svg-AaLwaVklfIvhXqUi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AaLwaVklfIvhXqUi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AaLwaVklfIvhXqUi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AaLwaVklfIvhXqUi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AaLwaVklfIvhXqUi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AaLwaVklfIvhXqUi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AaLwaVklfIvhXqUi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AaLwaVklfIvhXqUi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AaLwaVklfIvhXqUi .marker.cross{stroke:#333333;}#mermaid-svg-AaLwaVklfIvhXqUi svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AaLwaVklfIvhXqUi p{margin:0;}#mermaid-svg-AaLwaVklfIvhXqUi .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-AaLwaVklfIvhXqUi .cluster-label text{fill:#333;}#mermaid-svg-AaLwaVklfIvhXqUi .cluster-label span{color:#333;}#mermaid-svg-AaLwaVklfIvhXqUi .cluster-label span p{background-color:transparent;}#mermaid-svg-AaLwaVklfIvhXqUi .label text,#mermaid-svg-AaLwaVklfIvhXqUi span{fill:#333;color:#333;}#mermaid-svg-AaLwaVklfIvhXqUi .node rect,#mermaid-svg-AaLwaVklfIvhXqUi .node circle,#mermaid-svg-AaLwaVklfIvhXqUi .node ellipse,#mermaid-svg-AaLwaVklfIvhXqUi .node polygon,#mermaid-svg-AaLwaVklfIvhXqUi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AaLwaVklfIvhXqUi .rough-node .label text,#mermaid-svg-AaLwaVklfIvhXqUi .node .label text,#mermaid-svg-AaLwaVklfIvhXqUi .image-shape .label,#mermaid-svg-AaLwaVklfIvhXqUi .icon-shape .label{text-anchor:middle;}#mermaid-svg-AaLwaVklfIvhXqUi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AaLwaVklfIvhXqUi .rough-node .label,#mermaid-svg-AaLwaVklfIvhXqUi .node .label,#mermaid-svg-AaLwaVklfIvhXqUi .image-shape .label,#mermaid-svg-AaLwaVklfIvhXqUi .icon-shape .label{text-align:center;}#mermaid-svg-AaLwaVklfIvhXqUi .node.clickable{cursor:pointer;}#mermaid-svg-AaLwaVklfIvhXqUi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AaLwaVklfIvhXqUi .arrowheadPath{fill:#333333;}#mermaid-svg-AaLwaVklfIvhXqUi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AaLwaVklfIvhXqUi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AaLwaVklfIvhXqUi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AaLwaVklfIvhXqUi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AaLwaVklfIvhXqUi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AaLwaVklfIvhXqUi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AaLwaVklfIvhXqUi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AaLwaVklfIvhXqUi .cluster text{fill:#333;}#mermaid-svg-AaLwaVklfIvhXqUi .cluster span{color:#333;}#mermaid-svg-AaLwaVklfIvhXqUi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AaLwaVklfIvhXqUi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AaLwaVklfIvhXqUi rect.text{fill:none;stroke-width:0;}#mermaid-svg-AaLwaVklfIvhXqUi .icon-shape,#mermaid-svg-AaLwaVklfIvhXqUi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AaLwaVklfIvhXqUi .icon-shape p,#mermaid-svg-AaLwaVklfIvhXqUi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AaLwaVklfIvhXqUi .icon-shape .label rect,#mermaid-svg-AaLwaVklfIvhXqUi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AaLwaVklfIvhXqUi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AaLwaVklfIvhXqUi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AaLwaVklfIvhXqUi :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
能,延迟可接受
不能,存量 MySQL
是,高并发
是,低并发同机房
否
是,长流程
否,CRUD 跨库
是
新需求:跨服务写多处
能换成分布式数据库?
用 TiDB/OceanBase 原生事务业务零改
要强隔离?钱/库存扣减
TCC冻结 + 幂等三件套
XA / Seata XA
流程长?多步可补偿
Saga 编排状态机 + 补偿 + 对账
Seata AT首选,加 undo_log 即用
下游还能异步?通知/记账
再叠 Outbox+CDC发 MQ 解耦
三个端到端案例,每个都按“初始数据→正常走→挂了怎么补→对账怎么扫”讲透。表结构沿用第 8 章那套,订单号、用户号全写死,方便对照。
案例一:电商下单(Seata AT + Outbox,主链路同步、通知异步)
场景:用户 U100 下单 O2026001,买 SKU-A 一件 100 元。涉及 order-db(订单)、account-db(余额)、stock-db(库存)。要求下单时三处原子,支付成功后的积分、仓储走 MQ 异步。
初始数据:
— account-db.account
user_id | balance | frozen | version
U100 | 500.00 | 0.00 | 10
— stock-db.stock
sku | available | frozen
SKU–A | 50 | 0
— order-db.order_info:空,还没这单
正常流程(Seata AT):
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder() {
orderDao.insert("O2026001", "TRYING", 100); // 库1
accountService.deduct("U100", 100); // RPC,XID 透传
stockService.freeze("SKU-A", 1); // RPC,XID 透传
orderDao.updateStatus("O2026001", "PAID"); // 收尾
outboxDao.insert("order", "O2026001", "order_paid", "{…}"); // 同库事务
}
AT 框架在背后干的事(以 account 分支为例):
这时三张表变成:
account:U100 | 400.00 | 0.00 | 11
stock: SKU–A | 49 | 0
order: O2026001 | PAID | xid=192.168.1.10:8091:123456
outbox: id=987 | order_paid | NEW
CDC(Canal/Debezium)抓到 outbox 的 binlog,转成 MQ 消息 order_paid(O2026001),积分服务消费:
// 积分消费端:先判幂等,再干活
if (consumedDao.exists("order_paid:O2026001")) return; // 重复投递直接丢
pointsDao.add("U100", 10);
consumedDao.insert("order_paid:O2026001");
故障推演 A——RM 在一阶段后挂了:account 本地提交了,stock 还没收到二阶段,TC 挂了。重启后 TC 读自己的 global_table,发现 xid=123456 状态是 COMMITTING(只记了提交决议,还没通知完),就重新给 stock 发 commit。stock 的 RM 启动时扫本地 undo_log,发现 branch 还在,直接问 TC “我这个 xid 到底提交没”,TC 答提交,stock 补提交。收敛靠的是“TC 的决议表 + RM 的 undo 表”两边互相问,不是靠运气。
故障推演 B——下游积分挂了:MQ 按指数退避重试 1s、5s、30s…16 次后进死信 DLQ%order_paid。看板上死信数 +1 并电话告警,人工在后台点“重投”,积分服务恢复后重新消费。因为消费端有 consumed 去重表,重投 10 遍也只加 10 分。
对账 SQL(每天 02:00 跑,扫悬账):
— 1. 订单 PAID 但 outbox 没发出去(CDC 掉了)
SELECT order_id FROM order_info o
WHERE o.status='PAID' AND NOT EXISTS
(SELECT 1 FROM outbox b WHERE b.aggregate_id=o.order_id AND b.event_type='order_paid' AND b.status IN ('NEW','SENT'))
AND o.gmt_create < NOW() – INTERVAL 10 MINUTE;
— 2. 订单 TRYING 超 30 分钟(分支 Actions 悬挂,大概率是某 RPC 丢了 XID)
SELECT order_id, xid FROM order_info
WHERE status='TRYING' AND gmt_create < NOW() – INTERVAL 30 MINUTE;
— 3. 钱货对不上:订单收了 100,但账户没扣(AT 回滚失败的极端情况)
SELECT o.order_id FROM order_info o JOIN account_freeze f ON f.order_id=o.order_id
WHERE o.status='PAID' AND f.status != 'DEDUCTED';
第 2 条扫出来的单,人工看 Seata 控制台:分支缺了 stock,就手工调 stockService.freeze 补上或全单 Cancel。这就是第 7 章说的“对账是最后一道网”——AT 再顺滑,也不能省。
案例二:银行转账 A→B 100 元(TCC,强隔离、高并发)
场景:A(uid=1)给 B(uid=2)转 100。余额表就是第 8 章的 account(balance, frozen) + account_freeze 明细。要求并发下不能超扣,超时重试不能多扣。
初始:A: balance=1000 frozen=0,B: balance=200 frozen=0。
Try 阶段(冻结,不真扣):
— Try(A):同一事务写两行
UPDATE account SET frozen = frozen + 100 WHERE user_id = 1 AND balance – frozen >= 100;
INSERT INTO account_freeze(user_id, order_id, amount, status) VALUES (1, 'TX2026001-A', 100, 'FROZEN');
INSERT INTO tx_state(xid, branch_key, state) VALUES ('xid-tcc-001', 'TX2026001-A', 'TRY_DONE');
— Try(B):入账额度也先冻结(防止 B 账户被冻结/销户还敢收)
INSERT INTO account_freeze(user_id, order_id, amount, status) VALUES (2, 'TX2026001-B', 100, 'FROZEN');
INSERT INTO tx_state(xid, branch_key, state) VALUES ('xid-tcc-001', 'TX2026001-B', 'TRY_DONE');
这时查账:A 可用=900(1000-100),B 可用还是 200(入账冻结不增加可用)。别的转账想动 A,只能看到 900 可用,超扣被 balance – frozen >= 100 挡掉——这就是 TCC 用业务冻结代替 DB 行锁的精髓。
Confirm 阶段(全 Try 成功才进):
— Confirm(A):实扣
UPDATE account SET balance = balance – 100, frozen = frozen – 100 WHERE user_id = 1;
UPDATE account_freeze SET status='DEDUCTED' WHERE order_id='TX2026001-A';
UPDATE tx_state SET state='CONFIRM_DONE' WHERE xid='xid-tcc-001' AND branch_key='TX2026001-A';
— Confirm(B):实加
UPDATE account SET balance = balance + 100 WHERE user_id = 2;
UPDATE account_freeze SET status='DEDUCTED' WHERE order_id='TX2026001-B';
终态:A: 900/0,B: 300/0。
故障推演——空回滚(生产最常见的 TCC 事故):Try(A) 的 RPC 包在网络里堵了 6 秒,协调者 3 秒超时,判定全局失败,给 A 发 Cancel。A 查 tx_state 无记录,按规矩记空回滚并回成功:
INSERT INTO tx_state(xid, branch_key, state) VALUES ('xid-tcc-001', 'TX2026001-A', 'CANCEL_DONE');
— account_freeze 无行,不解冻,直接返回成功
第 7 秒,迟到的 Try(A) 终于到了。A 先查 tx_state,发现已有 CANCEL_DONE,直接拒绝,不写冻结。这单的钱没被 frozen 住,也就没有悬挂。如果没这一步,Try 后到真冻结了 100,但全局早已结束,这 100 就永远 frozen——线上管这个叫“钱丢了”,DBA 要手工 RELEASED。
A 账户服务协调者A 账户服务协调者#mermaid-svg-IRvYiHpDNsJxCMlj{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IRvYiHpDNsJxCMlj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IRvYiHpDNsJxCMlj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IRvYiHpDNsJxCMlj .error-icon{fill:#552222;}#mermaid-svg-IRvYiHpDNsJxCMlj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IRvYiHpDNsJxCMlj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IRvYiHpDNsJxCMlj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IRvYiHpDNsJxCMlj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IRvYiHpDNsJxCMlj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IRvYiHpDNsJxCMlj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IRvYiHpDNsJxCMlj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IRvYiHpDNsJxCMlj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IRvYiHpDNsJxCMlj .marker.cross{stroke:#333333;}#mermaid-svg-IRvYiHpDNsJxCMlj svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IRvYiHpDNsJxCMlj p{margin:0;}#mermaid-svg-IRvYiHpDNsJxCMlj .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IRvYiHpDNsJxCMlj text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-IRvYiHpDNsJxCMlj .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IRvYiHpDNsJxCMlj .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-IRvYiHpDNsJxCMlj .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-IRvYiHpDNsJxCMlj .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-IRvYiHpDNsJxCMlj #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-IRvYiHpDNsJxCMlj .sequenceNumber{fill:white;}#mermaid-svg-IRvYiHpDNsJxCMlj #sequencenumber{fill:#333;}#mermaid-svg-IRvYiHpDNsJxCMlj #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-IRvYiHpDNsJxCMlj .messageText{fill:#333;stroke:none;}#mermaid-svg-IRvYiHpDNsJxCMlj .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IRvYiHpDNsJxCMlj .labelText,#mermaid-svg-IRvYiHpDNsJxCMlj .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-IRvYiHpDNsJxCMlj .loopText,#mermaid-svg-IRvYiHpDNsJxCMlj .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-IRvYiHpDNsJxCMlj .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-IRvYiHpDNsJxCMlj .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-IRvYiHpDNsJxCMlj .noteText,#mermaid-svg-IRvYiHpDNsJxCMlj .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-IRvYiHpDNsJxCMlj .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IRvYiHpDNsJxCMlj .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IRvYiHpDNsJxCMlj .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-IRvYiHpDNsJxCMlj .actorPopupMenu{position:absolute;}#mermaid-svg-IRvYiHpDNsJxCMlj .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-IRvYiHpDNsJxCMlj .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-IRvYiHpDNsJxCMlj .actor-man circle,#mermaid-svg-IRvYiHpDNsJxCMlj line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-IRvYiHpDNsJxCMlj :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}3s 超时,判全局失败Try freeze(A,100)(包堵在路上)1Cancel xid-tcc-0012无 TRY 记录,记 CANCEL 空回滚回成功3Try freeze(A,100)(迟到 7s 到达)4查到 CANCEL_DONE拒绝冻结,回失败但不改账5
Cancel 的 SQL 同样要幂等:UPDATE account_freeze SET status='RELEASED' WHERE order_id=? AND status='FROZEN',重复调第二次 status 已是 RELEASED,影响 0 行,直接回成功即可。金额一律 DECIMAL(18,2),别用 float/double,对账差一分,银行能把电话打爆。
案例三:旅行预订机票+酒店+租车(Saga 编排,补偿不等价)
场景:订单 T2026001,三步:T1 订机票 800 元、T2 订酒店 400 元、T3 租车 200 元。每步本地提交,失败倒着补偿。注意补偿不是回滚,是“再做一笔逆向业务”。
正常流:
#mermaid-svg-z7cmAYn60634ApwE{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-z7cmAYn60634ApwE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-z7cmAYn60634ApwE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-z7cmAYn60634ApwE .error-icon{fill:#552222;}#mermaid-svg-z7cmAYn60634ApwE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-z7cmAYn60634ApwE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-z7cmAYn60634ApwE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-z7cmAYn60634ApwE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-z7cmAYn60634ApwE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-z7cmAYn60634ApwE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-z7cmAYn60634ApwE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-z7cmAYn60634ApwE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-z7cmAYn60634ApwE .marker.cross{stroke:#333333;}#mermaid-svg-z7cmAYn60634ApwE svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-z7cmAYn60634ApwE p{margin:0;}#mermaid-svg-z7cmAYn60634ApwE .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-z7cmAYn60634ApwE .cluster-label text{fill:#333;}#mermaid-svg-z7cmAYn60634ApwE .cluster-label span{color:#333;}#mermaid-svg-z7cmAYn60634ApwE .cluster-label span p{background-color:transparent;}#mermaid-svg-z7cmAYn60634ApwE .label text,#mermaid-svg-z7cmAYn60634ApwE span{fill:#333;color:#333;}#mermaid-svg-z7cmAYn60634ApwE .node rect,#mermaid-svg-z7cmAYn60634ApwE .node circle,#mermaid-svg-z7cmAYn60634ApwE .node ellipse,#mermaid-svg-z7cmAYn60634ApwE .node polygon,#mermaid-svg-z7cmAYn60634ApwE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-z7cmAYn60634ApwE .rough-node .label text,#mermaid-svg-z7cmAYn60634ApwE .node .label text,#mermaid-svg-z7cmAYn60634ApwE .image-shape .label,#mermaid-svg-z7cmAYn60634ApwE .icon-shape .label{text-anchor:middle;}#mermaid-svg-z7cmAYn60634ApwE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-z7cmAYn60634ApwE .rough-node .label,#mermaid-svg-z7cmAYn60634ApwE .node .label,#mermaid-svg-z7cmAYn60634ApwE .image-shape .label,#mermaid-svg-z7cmAYn60634ApwE .icon-shape .label{text-align:center;}#mermaid-svg-z7cmAYn60634ApwE .node.clickable{cursor:pointer;}#mermaid-svg-z7cmAYn60634ApwE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-z7cmAYn60634ApwE .arrowheadPath{fill:#333333;}#mermaid-svg-z7cmAYn60634ApwE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-z7cmAYn60634ApwE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-z7cmAYn60634ApwE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-z7cmAYn60634ApwE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-z7cmAYn60634ApwE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-z7cmAYn60634ApwE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-z7cmAYn60634ApwE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-z7cmAYn60634ApwE .cluster text{fill:#333;}#mermaid-svg-z7cmAYn60634ApwE .cluster span{color:#333;}#mermaid-svg-z7cmAYn60634ApwE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-z7cmAYn60634ApwE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-z7cmAYn60634ApwE rect.text{fill:none;stroke-width:0;}#mermaid-svg-z7cmAYn60634ApwE .icon-shape,#mermaid-svg-z7cmAYn60634ApwE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-z7cmAYn60634ApwE .icon-shape p,#mermaid-svg-z7cmAYn60634ApwE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-z7cmAYn60634ApwE .icon-shape .label rect,#mermaid-svg-z7cmAYn60634ApwE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-z7cmAYn60634ApwE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-z7cmAYn60634ApwE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-z7cmAYn60634ApwE :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
T1:机票出票CA1234 座 12A
T2:酒店预订希尔顿 1 晚
T3:租车预订神州 1 天
Saga Succeed三单全 CONFIRMED
状态表(Saga 执行器自己维护,和业务表分开):
— saga_log
saga_id | step | action | status | payload
T2026001 | 1 | book_flight | DONE | 票号 CA1234–12A
T2026001 | 2 | book_hotel | DONE | 订单 H–8899
T2026001 | 3 | book_car | DOING | –
故障:T3 租车没车了,返回“库存不足”(不可重试错误)。执行器查 saga_log,倒序补偿:
— 补偿后业务表
flight_order:CA1234–12A | REFUNDED | refund=720 fee=80
hotel_order: H–8899 | CANCELED | refund=400
car_order: – | FAILED | –
saga_log: T2026001 | COMPENSATED |
如果 C1 退票接口也 500 了(补偿失败),Saga 进 Failed_Need_Manual,记死信 + 电话告警,执行器按 5min、30min、2h 重试补偿,最多 24 小时。超过还不行,转人工:在后台露出“票号+退票链接”,客服手工退。Saga 上线必须配这个人工台,不然第一个补偿失败就能把系统拖死。
协同式 vs 编排式的血泪:早期有用协同式(机票订完发 flight_booked 事件,酒店监听),出问题查一单要翻三个服务的日志,XID 还串不起来。改编排式后,执行器一张 saga_log 表就是全链路,客服输入 saga_id,一屏看到哪步挂、补到哪。这就是第 6 章推荐生产用编排式的原因——不是性能,是可观测。
案例四:XA 悬挂救火(半夜 DBA 被叫起来的那次)
场景:单体 + 双库(order 库、points 库),JTA/XA 跨库。某天发布后,SHOW ENGINE INNODB STATUS 里一堆 PREPARED 事务,订单表几行被锁死,新单全超时。
复盘时间线:
这个案例就是 2PC 阻塞的活标本:参与者回了 YES 就不能单方面反悔,只能等老大发话,老大没了就全僵住。 所以跨服务、跨机房、发版频繁的业务,别用 XA;真要用,把 TM 的存储、恢复、手工对账 SOP 先准备好。
13. 可观测、测试与运维:事务做对了,还要看得见
分布式事务最怕“以为成了,其实没成”。可观测是和协议同等重要的另一半。
必备三件套:
测试清单( chaos 必做):
- kill -9 随机杀:一阶段中间杀 TM、一阶段中间杀 RM、二阶段中间杀 TC,看恢复后能否收敛。Seata 的 file/store/db 三种存储恢复路径都要测。
- 网络分区 + 时钟跳变:用 tc 制造 5s 延迟 + 丢包,看重试是否幂等、全局锁超时是否误杀。
- 幂等轰炸:同一 Confirm 发 100 遍,余额只能变一次。用唯一键 + 并发脚本压。
- 补偿也失败:mock 补偿接口永远 500,看 Saga 是否进 Failed_Need_Manual 并告警,而不是无限重试拖死。
运维血泪三条:
- Seata server 必须集群 + DB 存储(file 丢了就真丢了),TC 的库要和业务库分开,别共用一个 MySQL。
- MQ 事务反查接口要轻(只查 tx_state 表),别在里面调下游 RPC,反查被打慢会拖死整个 broker。
- 大促前把全局锁超时调短、重试次数调少,宁可快速失败走补偿,也别让一个慢 SQL 拖住整条链路。
14. 未来:事务在消失,还是在换皮
有意思的趋势是:事务没有消失,只是在往下沉、往外扩。
- 往下沉:TiDB、OceanBase、CockroachDB 把分布式事务做进内核,业务层回到单机写法。Serverless 数据库(Neon、Aurora DSQL)更进一步,存储计算分离 + 日志即数据库,事务是底座能力。这条路适合新业务,老业务迁库成本是最大的坎。
- 往外扩:Dapr Workflow、Temporal、Camunda 把 Saga 编排做成基础设施,业务只写 activity + 补偿,状态、重试、定时由平台托管。长流程业务(审批、供应链、IoT 编排)用这个比手写 Saga 状态机省一个数量级。
- AI 与异构数据源:向量库、湖仓、SaaS API 也要进事务。做法是“事务 outbox 化”:一切副作用先记 log 再异步做,靠幂等 + 补偿收敛。严格 ACID 跨异构源头基本没戏,别硬刚。
一句话收束:能用数据库原生解决的,别上框架;能用 AT 解决的,别上 TCC;能异步的,别同步强一致。 事务方案的复杂度,永远和你锁住资源的时间成正比。锁得越久,死得越惨。
15. 一页纸总结
- 单机事务靠 WAL + 锁 + MVCC,分布式把共享内存、共享时钟、可靠网络全拿走了,所以难。
- CAP 不是三选二,是为分区做准备;BASE 是“先记小本本,事后算清”的务实派。
- 2PC/XA 强一致但阻塞,3PC 修不好,生产跨服务慎用 XA。
- 共识(Raft)管复制,2PC 管原子提交,Spanner 是两者的合体 + 硬件时钟。
- TCC 适合钱和库存(冻结三件套 + 防悬挂),Saga 适合长流程(直接干 + 倒着赔),消息最终一致适合异步(Outbox+CDC 现在最香)。
- Seata AT 是跨库 CRUD 的首选,XA 同机房可用,TCC 留给核心链路,Saga 留给长流程,再叠 Outbox 解耦通知。
- 幂等键 + 唯一索引 + 状态机 + 对账 job,一个都不能少。没有对账的柔性事务,都是裸奔。
- 选型按“换库 > AT > TCC/Saga > 硬刚 2PC”的顺序问,锁越短命越长。
写到最后再唠一句:分布式事务没有标准答案,只有账本。 strong 的方案账算得快但容易卡死,柔的方案账算得慢但活得久。你要做的不是选“最正确的”,而是选“出问题时你能讲清、能查到、能补上”的。XID 能串起来、对账能扫出来、补偿能跑通,比多背两个协议重要得多。
参考与延伸
- Jim Gray《Transaction Processing》、Phil Bernstein《Concurrency Control and Recovery》:事务理论的圣经,2PC、恢复、隔离全在里面。
- Hector Garcia-Molina《Saga 论文(1987)》:Saga 原文,十页纸,值得一读。
- Google Spanner(OSDI’12)、Percolator(OSDI’10)、Calvin(SIGMOD’12):数据库原生三座大山。
- Seata 官方文档与 sample(at/tcc/saga/xa):直接跑一遍,比看十篇文章管用。
- Debezium + Outbox Pattern(Chris Richardson《Microservices Patterns》第 4 章):消息最终一致的工程手册。
- Martin Kleppmann《Designing Data-Intensive Applications》第 7/9 章:一致性与共识讲得最透的入门书。
网硕互联帮助中心




评论前必须登录!
注册