先讲一个我自己经历过的线上事故。某次大促前压测,订单服务和库存服务早就拆库了,用户下单后订单库已经写入成功,库存扣减却因为数据库连接池被打满而失败。结果就是订单显示“已支付”,仓库里根本没有货可发,客诉电话直接被打爆。当时团队里有人提出用分布式事务解决,结果方案评审吵了一下午,2PC、TCC、消息队列各种名词满天飞,新手听得一脸懵,老手也拿不定主意。
分布式事务这个问题,确实是后端开发绕不过去的坎。它本质上就是回答一件事:当订单、库存、账户这些数据散落在不同数据库甚至不同服务里,怎么保证它们要么一起成功、要么一起失败。这篇东西我就把目前业界最常用的6种方案——从强一致的2PC,到最终一致的本地消息表和MQ事务消息,再到兜底的最终对账——全部拆开揉碎讲清楚,包括原理、流程、适用场景、以及我实际踩过的坑。适合正在做技术选型的朋友、面试前突击的开发者、以及被跨库数据不一致问题折磨到失眠的运维和架构师。
1. 先把问题定义清楚:分布式事务到底在解决什么
很多人在学分布式事务之前,连“为什么要引入分布式事务”都没想明白,直接上来背2PC协议,背完就忘,遇到真实场景还是不知道怎么选。这一节先把问题的边界画清楚。
1.1 一个订单引发的数据一致性事故
不搞抽象的理论,直接看一个最常见的电商场景。用户在APP上下单买了一台手机,涉及的操作包括:创建订单、扣减库存、扣减用户余额、给用户增加积分。在单体应用时代,所有表都在同一个数据库里,一个BEGIN TRANSACTION包住四条SQL,要么全部提交,要么全部回滚,数据库的ACID帮你扛住一切。
可是业务一拆分,订单表在订单库,库存表在库存库,余额在账户库,积分在会员库。四个库可能分布在四台不同的机器上,甚至分别由四个团队维护。这个时候,跨库的多条操作就不再受单个数据库的本地事务保护了。如果创建订单成功、扣减库存失败,你会看到订单显示“已支付”,但库存根本没扣掉,超卖风险直接拉满;如果扣了余额但积分没加上,用户会发现自己付了钱却少拿了积分,客诉又来了。
这就是分布式事务要解决的核心问题:跨多个独立数据源的操作,如何保持数据一致性。这里的“独立数据源”可以是多个数据库、多个消息队列、多个缓存,甚至多个微服务的外部API。只要一个业务动作需要修改两个及以上独立存储,就存在分布式事务的潜在需求。
1.2 CAP与一致性光谱:从强一致到最终一致
要理解分布式事务方案的演进,必须先理解一个基础理论:CAP定理。任何一个分布式系统,在一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)这三者之间,最多只能同时满足两个。
这里要注意,很多新手以为CAP是“三选二”,可以随心所欲地选。实际上,一旦系统是分布式的,网络分区就一定会发生——交换机抖动、机房断网、磁盘故障,这些不是概率问题,而是时间问题。所以P是必选项,真正能做选择的是:当分区发生时,你保C还是保A。保C就是拒绝请求、等服务恢复,保A就是返回旧数据、接受暂时不一致。
基于这个前提,业界把分布式事务方案按照一致性强度排成了一条光谱。最强的一端是2PC,它追求的是强一致:事务提交后,所有节点读到的一定是最新数据。中间地带是TCC和SAGA,它们属于柔性事务,允许在事务执行过程中存在短暂的不一致窗口,但最终会达成一致。再往下是本地消息表和MQ事务消息,它们干脆不去强求实时一致,而是通过异步机制实现最终一致。最后还有一个兜底手段——对账,它不在事务链路中,而是通过定时比对数据、发现差异、触发修复,把漏网之鱼捞回来。
这条光谱非常重要,因为它决定了你在选型时不能贪心。既想要银行级别的强一致,又想要互联网级别的高并发,在分布式环境下是不现实的。
1.3 劝退环节:这四类场景不需要分布式事务
我知道看到这里,很多人已经摩拳擦掌想把2PC上到生产环境了。但我在一线干了这么多年,见过太多把分布式事务用滥的案例,最后架构复杂度爆炸、性能直线下降、故障定位困难。所以必须先劝退一波:以下四类场景,根本不需要分布式事务。
第一类,所有操作仍然可以在同一个数据库里完成。很多团队在微服务化的时候,把本该属于一个业务域的表硬拆到了不同库,比如把订单表和订单明细表拆到两个库,结果每次查订单都要跨库join。这种拆法本身就是错的,不需要用分布式事务来兜底,应该先把表合并回去或者做垂直拆分。
第二类,业务可以接受短暂的不一致。用户更新头像、修改个人资料、同步推荐位配置,这类操作即使延迟几秒甚至几分钟才生效,用户感知也很弱,完全不需要分布式事务。硬上的话,为了这点小事增加几台协调者的运维成本,纯属给自己找麻烦。
第三类,可以通过异步化解一致性压力。比如用户下单后发送短信通知,短信发晚了用户根本不会在意。这种场景用普通MQ异步投递就够了,不需要把发短信和创建订单绑在同一个事务里。
第四类,非关键链路的数据同步。比如数据仓库的T+1报表同步、搜索引擎的增量索引更新,它们本身就是异步批量执行的,中间状态对业务没有任何影响。
说这些是为了强调一个观念:分布式事务是有代价的,它要么牺牲性能、要么牺牲实时一致性、要么大幅增加系统复杂度。能用消息队列解决就别上2PC,能单库解决就坚决别拆库。这个观念比记住任何一种方案都重要。
2. 强一致方案:2PC与3PC,原理谁都会背,坑不是谁都踩过
接下来进入正题。第一对方案是2PC和3PC,它们的目标是实现强一致。这也是面试最常问、但实际生产中用得最少的一对方案。
2.1 2PC的完整执行流程:准备与提交
2PC,全称Two-Phase Commit,两阶段提交。它的核心思想可以用一句话概括:先让大家表态,再让大家干活。整个事务由一个协调者(TM,Transaction Manager)和多个参与者(RM,Resource Manager)组成。
第一阶段叫准备阶段,也就是投票阶段。协调者向所有参与者发送事务预处理请求,参与者收到后开始执行本地事务,但不提交。在执行的过程中,参与者会写入Undo日志和Redo日志,把回滚所需要的信息先存下来,方便后续出问题时恢复。执行完毕后,参与者向协调者返回结果:同意提交就返回Yes,执行失败就返回No。
第二阶段叫提交阶段,也就是执行阶段。协调者根据所有参与者的投票结果做最终决定。如果所有参与者都返回Yes,协调者就向所有参与者发送Commit指令,参与者各自提交本地事务并释放锁资源。只要有一个参与者返回No或超时无响应,协调者就向所有参与者发送Rollback指令,参与者根据Undo日志回滚本地事务。
用文本图表示就是:
协调者 参与者1 参与者2 |--- 准备事务 ------------->| | |<-------- 准备OK ---------| | |--- 准备事务 -------------------------------->| |<------------------------------- 准备OK ------| |--- 提交事务 ------------->| | |<-------- 提交完成 ---------| | |--- 提交事务 -------------------------------->| |<------------------------------- 提交完成 -----|这个流程看起来挺周到的:每个人都确认自己没问题了,再一起提交,很有仪式感。学过数据库的同学可能会发现,这就是数据库XA规范的基本思想,它在单机多数据库、或者数据库与消息队列之间都能工作。也正因如此,2PC在一部分强调数据准确性的行业,比如银行转账、证券交易,仍然有落地场景。
2.2 为什么2PC没能在互联网大厂普及
原理这么优雅,为什么我开头说“实际生产中用得最少”?因为2PC的代价非常大,四个痛点基本上每一个都致命。
第一个痛点是同步阻塞。从准备阶段开始,参与者就要持有资源锁,而且必须等协调者通知才能释放。网络上传输有延迟、协调者处理有延迟,其他想访问这些数据的事务就只能一直等。想象一下,你去商场买东西,所有收银员都拿走了你的钱和商品,但告诉你“先别动,我等总部确认一下”,然后你就在柜台前站着,后面排队的人全都卡住。数据库的锁和连接池就是这样被耗尽的,高并发下系统吞吐量直接断崖式下跌。
第二个痛点是协调者单点故障。协调者是整个事务的“大脑”。如果协调者在第二阶段挂了,所有参与者都会一直持有锁,处于阻塞状态。数据库无法自动恢复,数据库管理员可能要手动处理这些未决事务。我在生产环境见过一次协调者进程OOM,结果十几个数据库连接全部卡住,业务链路全线瘫痪,恢复花了将近两个小时。
第三个痛点是脑裂问题,这可能是最要命的。当协调者发出提交指令后,网络发生分区,部分参与者收到了Commit指令并执行了提交,另一部分参与者没有收到指令,处于“等通知”状态。因为协调者不可用,这些参与者可能会根据超时时间自行回滚。结果就是同一批事务,一部分节点提交了,另一部分节点回滚了,数据最终不一致。这已经违背了分布式事务的初衷。
第四个痛点是性能损耗。一次2PC事务至少经历两轮网络往返加两轮磁盘同步,事务时延比本地事务高出一个数量级。为了强一致把性能降到这个水平,绝大多数互联网业务都无法接受。
所以2PC适合并发量不高、节点数量有限、但数据一致性要求极高的场景。比如同一个机房内部几个数据库之间的强一致同步,或者Oracle、MySQL这类关系型数据库之间的XA事务。如果你们的业务是跨机房、跨地域的分布式部署,2PC基本可以直接划掉。
2.3 3PC:加了超时机制,结局也没好到哪去
3PC,三阶段提交,是2PC的改进版。它把2PC的准备阶段进一步拆成了CanCommit和PreCommit两个阶段,并且给参与者和协调者都引入了超时机制。这样做的目的是解决2PC中“协调者挂掉后参与者无限期等待”的问题。
流程上,3PC的三个阶段是:CanCommit阶段,协调者先问大家“能不能提交”,参与者只做预检查不执行SQL;PreCommit阶段,协调者确认所有参与者都能提交后,通知大家执行本地事务并写入Undo日志,但依然不提交;DoCommit阶段,参与者收到Commit指令后真正提交事务。如果协调者在PreCommit之后挂了,参与者等待超时会自动执行提交,而不是无限期阻塞。
听起来比2PC进步了不少,对吧?但3PC依然没有解决脑裂问题。假设协调者发出了PreCommit指令,两个参与者都执行了本地事务,随后网络分区把协调者和其中一部分参与者隔开。没收到DoCommit指令的那部分参与者超时后自行提交,但协调者这边因为收不到完整响应,可能已经决定回滚。结果同样是有的节点提交、有的节点回滚,数据不一致的问题依然存在。
所以3PC在真实生产环境中落地极少,它更多是分布式事务理论演进中的一个里程碑。面试时能讲清楚“2PC阻塞、3PC引入超时但仍有脑裂风险”这个演进逻辑,就够了。真正在生产环境扛起柔性事务大旗的,是接下来要讲的TCC和SAGA。
3. 柔性事务实战:TCC与SAGA,选型前先想清楚这三点
从这一段开始,我们进入实战领域。TCC和SAGA都属于柔性事务:不追求强一致,而是通过业务层的补偿逻辑,让系统最终达到一致状态。它们最大的优点是,不需要长时间持有数据库锁,性能和可用性都比2PC好很多。
3.1 TCC的三步舞曲:Try、Confirm、Cancel
TCC,全称Try-Confirm-Cancel,翻译过来就是“尝试-确认-取消”。它的核心思想是:让业务资源提前预留,等到所有分支都准备完成后,再真正执行;如果任何一步失败,就反向释放预留资源。
拿用户下单扣库存来举例。假设库存有100件,用户要买10件。TCC的两个分支分别是订单服务、库存服务。
Try阶段,订单服务创建一张预订单,状态是“待确认”;库存服务会锁定10件库存,把可下单库存从100改成90,同时把锁定库存变成10。这个阶段做的事情是“预留”,不是真的扣减,所以并不会减少可用库存总量。
Confirm阶段,如果Try全部成功,订单服务把预订单状态改成“已确认”,库存服务把锁定库存改成实际扣减,可用库存还是90。这个阶段做的事情才是真正提交。
Cancel阶段,如果任何一个Try失败,订单服务把预订单状态改成“已取消”,库存服务把锁定的10件库存释放回可用库存,100件恢复原样。
这个设计的好处是,从Try到Confirm之间,数据库没有任何长时间锁,资源提前预留之后,其他事务依然可以操作剩余库存。相比2PC,并发能力大大提升。
3.2 TCC最容易翻车的三个问题:空回滚、幂等、悬挂
TCC虽然好用,但它把一致性保证从数据库层面搬到了业务层面,也就意味着业务代码必须处理各种边界场景。我实际做TCC方案时,几乎每次都会被三个问题坑到:空回滚、幂等、悬挂。
第一个是空回滚。指的是Try阶段因为网络超时没有执行成功,但Cancel阶段却被调用了。比如库存服务的Try请求发出后,协调者等了很久都没收到响应,判断超时后直接走了Cancel分支。其实Try请求已经到达库存服务了,只是响应回包丢了。如果Cancel逻辑直接去释放库存,但Try根本没有锁过库存,就会出现负数的预占库存。解决办法是,在Try执行之前,先写入一条事务控制记录,状态为“尝试中”;Cancel执行时先查这条记录,如果记录不存在,说明Try没执行,直接返回成功,不回滚任何东西。
第二个是幂等。Confirm和Cancel在网络异常时可能被重复调用,如果每次调用都把库存多扣一次或多释放一次,数据就全乱了。TCC的所有接口都必须做幂等设计,常用方案是给每个事务生成全局唯一的事务ID,处理前先去事务控制表查一下,如果已经处理过就忽略,或者用数据库唯一键约束保证同一事务只处理一次。
第三个是悬挂。这是最隐蔽的一个问题。网络抖动可能导致Cancel先于Try到达。比如订单服务先超时回滚,走了Cancel分支,但库存服务的Try请求因为网络阻塞,在Cancel之后才到达。如果Try执行了,就会把已经释放的库存再次锁定,但后续永远不会有Confirm或Cancel来释放它,这个资源就悬挂住了。解决办法是,在Try执行前检查事务控制记录,如果记录已经存在并且状态是“已取消”,就直接拒绝执行Try,返回成功但不做任何锁定。
所以TCC不是简单的三个方法,它需要配套一张事务控制表,记录每个事务分支的状态流转。这张表通常长这样:
| 字段 | 说明 |
|---|---|
| tx_id | 全局事务ID,幂等控制的关键 |
| branch_id | 分支事务ID,用于识别具体参与者 |
| status | 状态枚举:TRYING、CONFIRMING、CANCELLING、FINISHED |
| try_time | Try执行时间,用于判断空回滚 |
| confirm_time | Confirm执行时间 |
| cancel_time | Cancel执行时间 |
这张表是TCC的定海神针,空回滚、幂等、悬挂全靠它解决。没经历过这三个问题的人,很难理解TCC为什么比想象的复杂——它表面上是三个接口,实际上是整个事务状态机的管理。
3.3 SAGA事务:长流程的逆向补偿艺术
如果说TCC是把2PC的锁提前换成资源预留,那么SAGA就是彻底的“不锁了,错了再改”。
SAGA的核心思想是:把一个长事务拆成一系列有序的本地事务,每个本地事务都有对应的反向补偿操作。正向流程依次执行,如果某一步失败,就沿着已经执行过的步骤一步步执行补偿,把数据退回到初始状态。
SAGA有两种主流的落地模式。第一种叫编排模式,也叫Orchestration,由一个中央编排器负责发布指令,指挥每个参与者执行本地事务。比如下单流程中,编排器依次调用订单服务、库存服务、账户服务;如果账户服务失败,编排器反向调用库存服务的退款/释放操作、订单服务的取消操作。这种模式的好处是业务流程集中管理,状态清晰,适合业务相对固定的场景。我自己的项目基本都用编排模式,因为出问题时日志好查、流程好控制。
第二种叫协同模式,也叫Choreography,没有中央编排器,每个服务执行完本地事务后,通过消息事件触发下一个服务。比如订单服务创建订单后发布“订单已创建”事件,库存服务订阅后扣库存,再发布“库存已扣”事件,账户服务订阅后扣余额。如果账户服务扣余额失败,它就发布“余额不足”事件,库存服务订阅后回滚库存。这种模式的好处是服务间解耦,新增一个参与者只影响相临链路,但缺点是流程分散在各服务中,业务不直观,排查问题时往往要把事件链从头翻到尾。
SAGA有一个天然的优点:因为不持有数据库锁,它天然适合长事务场景。比如一个下单链路要经过订单、库存、账户、物流四个系统,如果走2PC,四个库全都要锁住,任何一个慢节点都会拖垮整个事务;但SAGA中每个本地事务执行完就提交,释放锁,整体吞吐量可以保持在高位。
3.4 SAGA补偿的可靠性设计
SAGA的补偿逻辑虽然灵活,但它比TCC更依赖流程设计的严谨性。我总结下来有三个必须重视的方面。
首先是补偿操作必须实现幂等。补偿可能因为网络超时被重复触发,如果每次补偿都让库存+10,而实际上正向流程只扣过一次,那库存就凭空多了几十件。解决办法和TCC一样,用一个全局事务ID做去重。
其次是补偿记录的持久化。SAGA的正向流程和反向流程都要落日志,每一步执行前的状态、执行后的结果、执行的时间戳,都要写入事务日志表。这样即使某个服务重启、宕机,恢复后也能根据日志判断下一步该执行服务还是执行补偿。
第三是人工干预通道。SAGA补偿也可能失败,比如库存已经物理盘点了、优惠券已经过期了、退款账号已经注销了,这些情况下自动化补偿解决不了,必须进入人工处理队列。好的SAGA框架一定会提供管理后台或者告警接口,告诉运维人员“这里有一条死信事务,需要手工处理”。
TCC和SAGA怎么选?我的经验是:如果业务操作高度固定、分支数量少、且需要实时反馈结果,优先选TCC,因为它能在Try阶段尽早发现异常,避免长流程做了一半再回滚;如果业务流程长、涉及多个团队、服务间异步交互频繁,优先选SAGA,让每个服务专注自己的本地事务和补偿逻辑,更符合微服务架构的边界。还是拿下单举例,一个只扣库存+扣余额的短流程,用TCC比较合适;一个从下单到出库再到账再到干洗收衣的完整订单生命周期,选SAGA更灵活。
4. 最终一致落地方案:本地消息表、MQ事务消息,最后靠对账收尾
说完了强一致和柔性事务,下面来到日常开发中最常用、也最容易上手的最终一致方案。这一段的宗旨就一句话:如果业务能接受异步化,就没必要追求实时强一致,把“改数据”和“发消息”做成一个原子操作就够了。
4.1 本地消息表:最朴素但有效的最终一致方案
本地消息表的核心思路非常朴素:把业务数据和待发送的消息放在同一个数据库里,通过本地事务保证它们同时成功或同时失败,然后启动一个定时任务,把消息表里的记录不断投递给下游服务。
具体流程是这样的。订单服务在创建订单时,同时向本地数据库插入两条记录:一条是订单数据,一条是确认订单消息。这两条插入在同一个数据库事务里。事务提交后,一个异步发送程序扫描消息表,发现“待发送”状态的消息,就把消息内容投递到MQ或者直接调用库存服务的接口。
如果投递失败,消息状态还是“待发送”,定时任务会在下一轮继续扫描重发。为了不让消息无限重发,可以给消息表加一个重试次数字段,超过最大重试次数后,把状态置为“人工处理”,由告警通知运维人员介入。
伪代码大概长这样:
@Transactional public void createOrderTx(OrderDO order, OrderMessageDO msg) { orderMapper.insert(order); msgMapper.insert(msg); } public void scanAndSend() { List<OrderMessageDO> messages = msgMapper.selectByStatus(PENDING); for (OrderMessageDO msg : messages) { try { sendToMq(msg.getContent()); msgMapper.updateStatus(msg.getId(), SENT); } catch (Exception e) { msgMapper.increaseRetryCount(msg.getId()); } } }这个方案最大的优点是实现极其简单,不需要引入额外的分布式事务框架,也不需要改造MQ。缺陷有两个:一是消息表跟业务表耦合在同一个库,当业务数据量增大时,消息表会成为数据库的压力源;二是消息发送存在秒级延迟,因为定时任务的扫描周期一般不会设置得太短。但对于大部分对延迟不敏感的业务来说,本地消息表是性价比非常高的选择。
4.2 MQ事务消息:把半消息机制玩明白,就不用自己写定时任务
本地消息表方案虽然好用,但需要业务自己维护消息表和定时任务,并不算优雅。RocketMQ这类消息中间件提供的“事务消息”功能,可以把这个流程收进MQ内部,业务侧只需要关注发送半消息和执行本地事务。
RocketMQ事务消息的执行流程是:生产者先发送一条“半消息”,半消息对消费者不可见;接着执行本地事务;本地事务执行成功后,生产者向MQ发送Commit指令,此时半消息变成普通消息,消费者才能看到;如果本地事务执行失败,生产者发送Rollback指令,MQ删除半消息。
整个过程可以用文本图表示:
生产者 MQ 库存服务 |--- 发送半消息 ------------>| | |<-------- 半消息保存成功 ---| | |--- 执行本地事务 ---------->| | |--- 提交Commit消息 -------->| | | |--- 投递真实消息 ------->| | | |-- 扣减库存这套流程里,如果生产者发送Commit消息之前宕机了,MQ长时间收不到Commit或Rollback,就会回调生产者的消息回查接口,询问“这条半消息到底要不要发”。这就要求生产者在收到回查时,去查本地事务是否已经提交,把最终状态如实上报给MQ。
用RocketMQ事务消息,业务就不需要自己维护消息表和定时任务了,消息的可靠性由MQ保证。但有个前提,业务一定要保证消费端是幂等的。因为MQ的投递语义是至少一次,消费者在极端情况下可能收到重复消息。比如库存服务消费扣库存消息后,回复ACK时网络超时,MQ会重新投递,如果消费端没有做幂等,库存就会被重复扣减。
在选型时,我的建议是:如果项目里没有引入RocketMQ,为了一个分布式事务专门搞一套RocketMQ集群,成本比较高,不如老老实实用本地消息表;如果项目里已经有RocketMQ,那事务消息是更干净的选择,省掉了自研定时扫描任务的维护成本。
4.3 最终对账:所有方案的最后一道安全网
不管选哪种分布式事务方案,我始终坚信一句话:任何方案都不可能100%保证一致,所以最终对账不是可选项,而是必选项。
对账系统的本质是:通过定时任务,把两个系统里的数据进行比对,找出不一致的记录,再触发修复流程。拿订单和库存举例,对账定时任务每天凌晨读取订单库里的“已支付未发货”订单明细,统计每种商品的订单数量;再去库存库里读取“已扣减”的库存流水,比对两者是否吻合。如果发现订单数量大于扣减数量,说明有订单扣库存失败但漏补偿了,触发修正流程:要么补扣,要么退款。
一个成熟的对账系统通常有三层结构。底层是对账底账,也就是各系统的流水数据,为了保证可比对,每个系统都需要提供统一的流水接口,字段包括业务ID、金额、数量、时间、状态。中间层是对账引擎,它负责拉取数据、比对差异、生成差异报告。上层是差异处理模块,根据差异类型自动修复或转人工。
我第一次做对账系统的时候,犯过一个非常低级的错误:只对总数不对明细。比如订单库总订单数是100单,库存库扣减流水也是100条,我就判定数据一致。结果有个订单扣了两次库存,另一个订单漏扣了一次,总数正好抵消。后来我改成按订单ID逐条比对,才把这些诡异的问题暴露出来。所以对账一定要明细级比对,不能只比对汇总数。
4.4 六种方案选型对比,一张表看清
到这里,六种方案全部讲完了。下面用一张表把它们的核心特性放在一起看,方便你直接参考。
| 方案 | 一致性强度 | 性能影响 | 业务侵入度 | 复杂度 | 典型场景 |
|---|---|---|---|---|---|
| 2PC | 强一致 | 极差,锁时间长 | 数据库XA,业务侵入中 | 高 | 银行转账、交易所核心 |
| 3PC | 强一致(仍有脑裂风险) | 差 | 数据库XA改进 | 高 | 理论上参考,落地少 |
| TCC | 最终一致,准实时 | 较好,资源预占 | 极高,需实现Try/Confirm/Cancel | 高 | 核心交易链路、资金账户 |
| SAGA | 最终一致,补偿延迟 | 较好,无锁 | 中高,需正向与补偿逻辑 | 中高 | 长流程业务、跨团队协作 |
| 本地消息表 | 最终一致,秒级 | 好,但消息表占DB压力 | 中,业务表与消息表同库 | 低 | 异步通知、积分、消息下发 |
| MQ事务消息 | 最终一致,毫秒级 | 好,MQ承担消息状态 | 中,需配合消息回查 | 中 | 订单与库存解耦、异步扣减 |
再补充一个实际选型时的判断口诀。资金敏感、链路短、要求实时失败的,选TCC;链路长、异步化场景多、能接受最终一致的,选SAGA;不需要实时反馈的异步通知,优先用MQ事务消息;没有MQ基础设施的,本地消息表兜底;所有的方案都用最终对账做最后防线。至于2PC和3PC,除非是强一致需求非常刚性的行业,否则不建议作为新系统第一选择。
最后再看一个常见问题速查表,这些都是我被问过无数次的问题,直接列出来:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 2PC中协调者挂了,所有事务卡死 | 参与者在等待协调者指令 | 引入超时机制;或改用3PC/TCC |
| TCC的Cancel重复执行导致数据错乱 | Cancel未做幂等 | 用事务ID查事务控制表去重 |
| TCC中Cancel先于Try到达 | 网络乱序 | Try执行前检查控制记录,已取消则直接拒绝 |
| 本地消息表消息重复投递 | 发送成功后ACK丢失 | 消费者做幂等,按业务ID去重 |
| RocketMQ事务消息一直处于半消息状态 | 本地事务未回查或者回查失败 | 检查消息回查接口是否正常返回本地事务状态 |
| 对账只比对总数,漏掉错误明细 | 汇总数掩盖单条数据差异 | 按业务ID明细级比对 |
我在实际项目中,最喜欢用的组合是:核心资金链路走TCC,非核心异步链路走RocketMQ事务消息,最后每天晚上跑一遍对账任务。这套组合经历过日请求量过亿的验证,数据一致率可以做到99.99%以上,剩下那0.01%由对账和人工兜底。分布式事务没有银弹,选型的本质是搞清楚你的业务能容忍多大程度的不一致、能接受多少性能损失,然后匹配最合适的那一种方案。