下单扣了库存,扣了余额,也写了订单——听起来一切正常。可当支付服务超时回滚,而库存服务已经提交,用户就会遇到一件诡异的事:钱扣了,货没了,订单还是”已支付”。这就是分布式事务要解决的问题。它不是某个框架的开关,也不是加一行注解就能搞定的事。它是当一次业务操作跨越多个独立服务、每个服务都有自己的数据库时,如何让这堆操作”要么全成、要么全败”的工程难题。这篇文章想把它的三条主流路线讲清楚。

一、为什么单机事务那一套,跨服务就失效了
单机数据库的事务之所以好用,是因为它有一个统一的”裁判”——数据库自己。ACID 里的原子性、隔离性、持久性,都由这同一个进程、同一份日志、同一把锁来保证。你只要把一个 BEGIN…COMMIT 写完,剩下的交给数据库。
但一旦操作跨了服务,局面就变了。库存服务用的是自己的 MySQL,支付服务连的是另一个库,它们之间只能通过网络调用通信。这时候出现了两个绕不开的困难。
第一个是”没有统一裁判”。没有任何一个进程能同时锁住两个库的表,也没有一个日志能同时记录两边做了什么。你无法用一个 COMMIT 让两边一起生效。
第二个更致命——网络不是可靠的。请求发出去之后,可能出现三种结果:对方收到了并成功执行、对方收到了但执行失败、以及最讨厌的第三种——你根本不知道对方到底有没有执行。超时的时候,请求可能压根没到,也可能到了、执行了、只是应答在回来的路上丢了。这种”结果未知”的状态,是所有分布式事务方案都必须正面处理的。
很多人第一反应是”加个重试不就行了”。可重试会带来新问题:如果第一次其实成功了,重试就是重复扣款。所以重试必须配上一个前提——操作要能识别”我已经做过了”,也就是幂等。分布式事务的复杂性,本质上就是围绕”结果未知”和”重复执行”这两个魔鬼展开的。
二、三条主流路线:从”尽量不改变世界”到”反过来补偿”

业界解决这个问题的思路,可以按”对业务侵入程度”和”一致性强度”排成一个光谱,最常用的三条路线是本地消息表、TCC 和 Saga。
第一条:本地消息表(最终一致)。它的核心思想非常朴素——不追求强一致,只求”最终一致”。做法是:在业务库里放一张消息表,业务操作和”写一条待发送消息”放在同一个本地事务里。因为是同一个库,这个原子性由数据库天然保证。事务提交后,再有一个后台任务不断把消息表里的记录投递出去,投递成功就标记完成。这样,只要本地事务成功,消息一定不会丢;下游收到消息后执行自己的本地事务即可。代价是:中间会有一段不一致的时间窗口,而且下游必须做到幂等。它适合那些”晚一秒对上没关系、但绝对不能丢”的场景,比如积分、通知、统计。
第二条:TCC(Try-Confirm-Cancel)。这是一个强侵入的方案,要求每个参与方都实现三个接口:Try 阶段做资源预留(比如冻结库存,而不是直接扣减),Confirm 阶段确认提交(真正扣减),Cancel 阶段取消释放(解冻)。协调者先调用所有参与方的 Try,全部成功再逐个 Confirm;任何一个 Try 失败,就调用所有已成功方的 Cancel。它把”不确定”压缩在 Try 到 Confirm 之间的短窗口里,一致性更强、响应更快。但代价是业务代码要为每个操作写三段逻辑,且要处理空回滚、悬挂、幂等这些边界情况,开发成本最高。它适合资金、库存这类对一致性要求极高的核心链路。
第三条:Saga(长事务拆分)。Saga 把一个长事务拆成一串本地事务,每个本地事务都配一个”补偿动作”。整条链路按顺序执行,一旦某一步失败,就反向依次执行前面所有步骤的补偿。它不像 TCC 那样要求资源预留,补偿动作是业务层面的”反向操作”(比如退款而不是解冻)。实现上更松散、更适合长流程,但因为没有隔离性,中间状态对外可见,且补偿逻辑往往比正向逻辑更难写——不是所有操作都能完美撤销。
三条路线没有绝对优劣,只有场景匹配。选型的依据很简单:一致性要多强、业务能改多少、失败概率有多高。
三、比选型更重要的三件事
很多人把分布式事务当成”选个方案装上”就完了,但真正决定系统稳不稳的,往往是几个容易被忽略的工程细节。
第一件:幂等是所有方案的地基。无论你选哪条路线,都绕不开”同一个操作可能被执行多次”。网络重试、消息重复投递、补偿重放,都会造成重复。所以要给每个操作一个全局唯一的业务 ID,服务端记录已处理的 ID,重复请求直接返回上次结果。没有幂等,再优雅的方案都会在重复扣款面前崩塌。
第二件:补偿不是万能的。Saga 和 TCC 都依赖”补偿”,但补偿有边界。发出去的短信撤不回来,已经生效的第三方接口很难反转,补偿动作本身也可能失败。所以设计时要问自己:这一步真的可补偿吗?如果不可补偿,就应该把它挪到整条链路的最后一步去做,让不可逆的操作尽可能靠后。
第三件:可观测性决定你能不能救火。分布式事务出问题时,故障往往散落在多个服务的日志里,没有全局视角根本拼不出真相。所以每一次事务都要有统一的 trace id 贯穿全链路,每个阶段的状态(Try 了没、Confirm 了没、补偿了没)都要落库可查。出事的时候,你需要的不是猜测,而是能一眼看到”卡在哪一步、哪一方还没确认”。这也是为什么很多团队最终发现:排查能力的价值,有时比方案本身还高。
再补一句常被忽略的现实:能用单机事务解决的,就不要引入分布式事务。把强相关的操作放进同一个服务、同一个库,用最朴素的本地事务搞定,永远是成本最低的选择。分布式事务是最后的手段,不是首选的美化。

结语
分布式事务之所以难,不是因为它算法复杂,而是因为它把分布式系统最本质的两个现实摆在了你面前:网络不可靠,状态不确定。所有方案——本地消息表、TCC、Saga——都是在用不同的方式,去和这两个现实讨价还价。有人用最终一致换来了简单,有人用业务侵入换来了强一致,有人用补偿换来了长流程的可行性。
真正成熟的工程师,不会问”哪个方案最好”,而是先问”这个业务到底能容忍多强的一致性、能接受多大的改造”。想清楚这一点,方案自然就浮出来了。而在所有方案之下,还有一条不变的底线:幂等、可观测、想清楚补偿的边界。把这三件事做好,比选对框架更让人安心。