大家好,我是晚安code。
假设一个场景:线上一个用户下单,余额扣了、库存也扣了,订单却显示未支付。排查到最后,是订单库、账户库、库存库三个库各改各的,谁也管不了谁。这是分布式事务上的第一课:跨库操作没有本地事务兜底,出问题只是早晚的事。
一、一个扣款场景,把问题逼出来
分布式事务要解决的,不是"一次操作改多个地方",而是"多个地方要么同时成功、要么同时失败"。
本地事务(Local Transaction):在同一个数据库内完成的事务,要么全部提交,要么全部回滚。你可以理解为"同一本账本上的记录,错了一整页都能涂掉重写"。
用生活里的场景类比:三个人合租,住在 1 楼、3 楼、5 楼,晚上要同时关灯,得挨个通知。只要中间有一层没听到,就有一个人摸黑进门。跨库操作就是这种"挨个通知"的活,而本地事务只会管自己那盏灯。
分布式事务(Distributed Transaction):跨越多个数据库或服务的操作集合,要求所有参与方要么一起成功、要么一起回滚。你可以理解为"几本分处各地的账本,想在同一时刻改对"。
这篇文章我打算把分布式事务从原理到落地串一遍:先看 CAP 定理为什么让这条路这么难走,再看 BASE 理论给出的妥协方案,最后对比 AT 模式和 TCC 模式这两种主流实现,以及它们各自的坑。收藏一下,我们开始。
二、CAP定理:分布式系统的天花板
在 CAP 定理里,分区容错是必选项,一致性和可用性只能二选一。
CAP 定理(Consistency, Availability, Partition tolerance):分布式系统不可能同时满足一致性、可用性、分区容错性,最多同时保证其中两个。你可以理解为"酒店的地点、价格、房间大小,最多让你挑两个满意"。
三个指标拆开看:
1)一致性(Consistency):用户访问任意节点,读到的数据必须一样。node01 改了余额,node02 必须跟着变,否则用户换个节点就查到旧账。
2)可用性(Availability):读或写总能成功。只能读不能写、只能写不能读,或者都执行不了,都算弱可用或不可用。
3)分区容错(Partition tolerance):节点之间的网络断了,系统也要继续对外服务。Partition 就是网络故障把系统劈成几个互不相通的"孤岛"。
关键矛盾在这:网络不会 100% 保证畅通,分区一定会出现;而系统又必须持续运行。所以分区容错是硬指标,所有分布式系统都得满足。剩下的,只能在一致性和可用性里挑一个:
- 选可用性(AP):节点继续读写,但同步不了的节点数据不一致,事后靠补偿慢慢收敛。
- 选一致性(CP):把数据锁住,等网络恢复再放行,期间系统不可用或只能读。
现实就是这么不讲道理——只要网络会断,你就得在 A 和 C 之间二选一。

把取舍画成流程图,一眼就能看懂。看图 1:网络分区一旦出现,往左走是 AP,数据先乱一阵再收敛;往右走是 CP,先锁住不动,一致但不可用。


三、BASE理论:不强一致,但最终一致
BASE 理论放弃的不是一致性,而是强一致性——把"必须立刻一致"换成"最后一致就行"。
BASE 理论(Basically Available, Soft State, Eventually Consistent):分布式系统允许损失部分可用性、出现中间状态,最终把数据收敛到一致。你可以理解为"先把事办了,睡前保证把账平掉"。
三个词各管一段:
- 基本可用(Basically Available):出故障时损失部分可用性,保住核心功能。抢购高峰让你排队等一会儿,而不是直接 502。
- 软状态(Soft State):允许存在中间状态,比如数据暂时对不上。
- 最终一致性(Eventually Consistent):不要求立刻一致,但中间状态结束之后,数据最终会一致。
顺着 BASE 的思路,分布式事务的解法分成两个方向:
- AP 思想:各分支各自执行、各自提交,不锁数据。允许结果不一致,之后用补偿手段恢复,实现最终一致性。AT 模式走这条路。
- CP 思想:各分支执行完先不提交,等彼此的提交结果,再一起提交或回滚。期间锁住资源,数据不可用,但保证一致。XA 模式走这条路。
所以分布式事务从来没有"一种标准答案",关键是先想清楚:你打算牺牲一致性,还是可用性?
四、AT模式:框架帮你补偿,但别忽视脏写
AT 模式最大的卖点是不用写补偿代码,代价是并发场景下容易踩脏写的坑。
AT 模式(Automatic Transaction):Seata 提供的一种分布式事务实现,利用数据快照自动回滚,开发者几乎无感。你可以理解为"系统定期给你的数据拍照,出事就按照片恢复"。
坦白讲,我最早就是无脑选了 AT 模式,因为它接入成本最低,几个注解就搞定。它分两个阶段走:
阶段一:每个分支事务执行前,先记录数据快照,再正常执行、正常提交。
阶段二:全局协调者汇总各分支的结果——全部成功,说明事务在阶段一已经提交完了,这里只需要删掉快照;只要有分支失败,就按快照把数据恢复到更新前,再删快照。
这个两阶段流程用文字说容易绕,看图 2,重点看阶段二的两个出口。

大多数场景下,AT 模式确实省心。但在极端情况,尤其是多线程并发访问同一个 AT 事务里的数据时,会出现脏写——某个分支回滚,把另一个事务刚写入的新值,一并覆盖回旧值了。
两个线程同时改同一条库存记录,一个事务回滚,另一个线程刚提交的新值被快照覆盖,账又对不上。排查了整整一天,最后锁定位在脏写。好家伙,快照回滚居然能滚出脏数据,这谁能想到?

解决办法是引入全局锁:在分支释放数据库锁之前,先拿到全局锁,保证同一时刻只有一个事务能操作这条数据,回滚时就不会踩到别人刚写的东西。
可能有人会问:AT 模式加了全局锁,并发性能不就废了吗?
这就是取舍。全局锁把冲突行的并发度压到 1,多个线程抢同一行时吞吐明显下降。以我现在的经验,写冲突不严重、读多写少的业务用 AT 很划算;写热点集中的场景,老老实实考虑 TCC 或者消息队列。
五、TCC模式:补偿逻辑自己写,更可控
TCC 模式牺牲了编码量,换来的是对补偿过程的完全掌控,适合高并发和补偿逻辑复杂的业务。
TCC 模式(Try-Confirm-Cancel):把分布式事务拆成预留、确认、取消三个阶段,三个方法都由开发者手写。你可以理解为"先交定金把东西占住,再付尾款,反悔了就退定金"。
三个方法各管一件事:
- try:检查并预留资源。比如检查余额够不够,够就先把要扣的金额冻结起来。
- confirm:真正完成业务。前提是 try 成功了,confirm 一定要能成功——confirm 里只放不会失败的收尾动作。
- cancel:释放预留的资源,是 try 的反向操作。
用一个扣款例子串一遍。账户 A 余额 100,要扣 30:
1)Try:余额充足,冻结 30,可用余额变 70。此时总余额(冻结 + 可用)还是 100,分支事务直接提交,不用等别的分支。
2)Confirm:可用余额已经扣过了,直接把冻结的 30 划走,总余额变 70。
3)Cancel:释放冻结,冻结的 30 归零,可用余额回到 100。
TCC 的核心思想一句话就能讲透:扣款不直接扣,先锁进一个"冻结"口袋。看图 4 会更直观。

但 TCC 的坑也藏在手写逻辑里。一个分布式事务里有两个分支,try 阶段分支 A 成功、分支 B 阻塞。阻塞太久,全局事务超时,二阶段 cancel 触发,两个分支都要执行 cancel——可分支 B 压根没执行过 try。
空回滚(Empty Rollback):分支事务还没执行 try 就被要求 cancel,此时 cancel 必须什么都不做。你可以理解为"客人还没进店,店员就开始退他定金了"。
更麻烦的是,分支 B 的 try 阻塞结束后还会继续执行——但全局事务已经结束了,永远不会有 confirm 或 cancel 再来。这个事务只执行了一半,悬在那里:
事务悬挂(Transaction Suspension):分支的 try 被阻塞,全局事务已超时结束,阻塞结束后 try 才执行,整个事务永远收不了尾。你可以理解为"主人全家都出门了,客人还站在门口按门铃"。
try 还没执行,cancel 先到了;主人走了,客人还在门口。这两个问题不处理,线上迟早出事。

好在解法也明确:写 try 和 cancel 时维护一张事务状态表。只有 try 成功记录过,cancel 才允许真正回滚;try 执行前先查状态,确认全局事务还活着再动手。
六、怎么选:没有银弹,只有取舍
分布式事务没有银弹,选方案的本质,是选"你能接受哪方面的损失"。
| 方案 | 补偿方式 | 代码量 | 并发表现 | 适合场景 |
|---|---|---|---|---|
| XA(CP) | 数据库两阶段提交 | 低 | 低(锁资源) | 金融级强一致 |
| AT 模式(AP) | 框架快照自动回滚 | 低 | 中 | 常规业务快速接入 |
| TCC 模式(AP) | 手写 try/confirm/cancel | 高 | 高 | 高并发、补偿逻辑复杂 |
绝大多数业务用 AT 模式就能撑住,重点别把它当银弹;写热点集中、补偿逻辑绕的场景,才值得为 TCC 多写那几个方法。
可能有人会问:消息队列(本地消息表、事务消息)算不算分布式事务方案?
算。它是另一种思路——先把本地改动落库,再把"要通知下游"这个动作也可靠地投递出去,靠消息做到最终一致。好处是彻底不锁资源,坏处是做不到同步强一致,下游消费有延迟。选哪种,看你的业务能接受多少延迟。
说白了,每一类分布式事务方案,都是在"一致性、可用性、代码量"三者里做减法。在我这边,能异步走消息队列的优先消息队列,其次 TCC,AT 模式适合快速接入,XA 只留给真正必须强一致的核心链路。
我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你在项目里用过哪种分布式事务方案,有没有被脏写或空回滚坑过?