分布式事务

发布于 作者: Ethan

分布式事务主要实现方式分析

在现代分布式系统中,一个业务操作往往涉及多个服务和数据库。如果没有合适的机制来保证它们要么一起成功、要么一起失败,就会出现数据不一致的问题。为了解决这一挑战,业界提出了多种分布式事务方案,包括两阶段提交(2PC)、三阶段提交(3PC)、TCC(Try-Confirm-Cancel)、Saga 以及 Outbox 本地事务消息等。下面我们将逐一分析这些方案的起源与背景、要解决的问题,使用通俗语言解释其工作机制并给出示例,并介绍各方案的主流实现,最后对比它们的优缺点和适用场景。

2PC(两阶段提交协议)

2PC起源与解决的问题

两阶段提交(2PC, Two-Phase Commit)是在数据库和事务处理中提出的经典算法,用于保证分布式环境下多个节点上的操作要么全部提交、要么全部回滚,实现事务的原子性。它由计算机科学家 Jim Gray 等人在20世纪70年代研究分布式数据库时提出,是一种原子提交协议,目前被广泛应用于关系数据库的 XA 事务、企业中间件的分布式交易管理等场景[1]。2PC 引入一个事务协调者(Coordinator)和多个事务参与者(Participant):协调者负责统一决策提交或回滚,参与者负责执行各自本地的事务操作。它试图解决的问题是:当一次业务操作需要更新多个不同数据库或服务时,如何确保这些更新要么全部成功,要么在任何一步失败时全部撤销,以避免部分成功部分失败的数据不一致情况。

2PC工作机制与示例

2PC 将事务的提交过程拆分为 两个阶段:

  • 阶段一:准备阶段(投票阶段)。协调者向所有参与者发送请求:“能否执行该事务并准备好提交?”,等待参与者答复。每个参与者收到请求后,执行本地事务操作(但先不提交),将操作的undo和redo日志记录下来,以便将来提交或回滚。如果参与者本地执行成功且做好提交准备,就回复“同意”(Yes);如果发生问题无法执行,则回复“中止”(No)。举例来说,假设电商系统中用户下单需要扣减库存和创建订单两个服务协同完成:协调者会询问“库存服务”和“订单服务”是否能执行这次事务(如库存服务检查库存是否足够,订单服务检查订单数据是否合法)。如果两方都回答“Yes”,则表示都做好了提交准备。
  • 阶段二:提交阶段。协调者汇总所有参与者的反馈:如果所有参与者都同意准备就绪,则协调者向所有参与者发送正式的“提交”指令,要求它们真正将刚才的本地事务提交;如果任何一个参与者在第一阶段回复了中止,或者有参与者无响应超时,则协调者发送“回滚”指令给所有参与者,要求它们撤销之前的操作。继续前述下单示例:如果库存服务和订单服务都同意,那么协调者发出“提交”命令,库存服务正式扣减库存、订单服务正式写入订单,并各自释放在整个事务期间加的锁,然后反馈给协调者提交完成;如果有一方不同意或超时无响应,那么协调者下达“回滚”命令,先前库存扣减和订单写入操作就利用阶段一记录的 undo 日志全部撤销。这样确保了两步操作要么都成功生效,要么都不生效。

在正常情况下,2PC 能确保多个节点最终达成一致,满足分布式事务的原子性要求。需要注意协调者在整个过程中起决策作用,各参与者在等待协调者指令时会阻塞相关资源。例如在阶段二提交前,各数据库的相关记录可能被锁定,不可并发修改,以确保提交时数据状态一致。这个过程直观上就像“一起准备好,一起执行”:先所有人表态能不能做,如果都能就一起正式做,如果有任何不行就都不做。

示例场景:假设用户要将100元从账户A转账到账户B,账户A在银行的转账服务,账户B在另一个支付服务。采用2PC协议时,转账事务的协调者会首先询问两个服务:“能否扣减A的100元并增加到B?” 两个服务分别检查:账户A是否有足够余额、账户B是否存在等。如果都回复“Yes可以”,协调者再通知两边“正式提交”,于是银行服务扣减A账户100元,支付服务增加B账户100元,然后都确认提交完成。如果中途任何一步有问题(例如账户余额不足导致A服务回复No),协调者将通知双方回滚——银行服务不会扣款,支付服务也不增加余额,整个事务取消。这样要么两个账户都改变,要么都不变,保持一致。

2PC 实现简单直接,但也有显著缺陷。首先是性能问题:参与者在事务提交完成之前一直处于阻塞状态,占用资源,其他并发请求可能被阻塞,牺牲了系统吞吐率。其次是可靠性问题:协调者是单点,一旦在提交过程中协调者宕机,参与者将不知道该做什么——特别是在第一阶段都同意但协调者没发出最终指令的情况下,参与者会一直锁着资源傻等,整个系统进入僵局。还有数据不一致风险:如果协调者在发出提交命令后宕机或网络分区,可能出现部分参与者收到提交并执行了,另一部分没收到而无法提交,导致数据一部分改了一部分没改,出现分布式的不一致。这种“不确定”状态需要人工干预才能处理。总的来说,2PC保证强一致性但代价高昂,不适合高并发高延迟敏感的场景。

2PC主流实现方案

由于2PC偏重于数据库层面的实现,很多关系型数据库和中间件已经内置支持2PC。例如:

  • XA规范:X/Open组织制定的分布式事务规范,主流数据库(如MySQL、Oracle、PostgreSQL等)和应用服务器都支持XA事务。通过 XA,应用可以使用Java的JTA(Java Transaction API)等接口,让多个数据库资源参与一个全局2PC事务,由事务管理器(如Atomikos、Narayana等)协调提交或回滚。
  • 分布式数据库:一些NewSQL数据库内部也采用类似2PC的原理保证跨分片事务一致性。如Google Spanner底层就使用两阶段提交配合精巧的时间戳协议来实现跨分区事务。
  • Seata AT 模式:Seata是阿里巴巴开源的分布式事务框架,提供了AT、TCC、Saga、XA等模式。一种默认的AT模式本质上就是改良的两阶段提交:一阶段拦截业务SQL并记录Undo Log日志,提交本地事务,同时锁定必要资源;二阶段再根据全局决策异步提交或利用Undo Log回滚。AT模式通过在一阶段就提交本地事务来缩短锁定时间,以提升性能,同时在需要回滚时通过补偿撤销改动,实现了2PC的效果。Seata 还支持标准 XA 模式,将 XA 接口应用于微服务场景。

需要强调的是,2PC在微服务架构中直接使用并不常见,因为微服务往往各自数据库独立、没有统一的事务协调者。但在传统单体应用或少数服务直接共享全局事务的场景下,2PC(例如通过Seata或全局事务管理器)依然是强一致性事务的解决方案之一。

3PC(三阶段提交协议)

3PC起源与解决的问题

三阶段提交(3PC, Three-Phase Commit)是对两阶段提交的改进协议。它由电脑科学家 Dale Skeen 在20世纪80年代提出,旨在解决2PC的一些缺陷,特别是消除协调者单点故障导致的参与者长时间阻塞以及减少事务悬而不决的状态。3PC在2PC的基础上引入了超时机制和一个额外的预备阶段。通过引入第三个阶段,3PC尝试确保在最终提交之前,所有参与者的状态都保持一致,即便出现协调者故障,参与者也能根据超时自行决定下一步,从而降低陷入无限期阻塞的风险。

简单来说,3PC把2PC的第一阶段再拆成两个步骤,使协议流程从两阶段变为三个阶段,分别称为CanCommit(询问阶段)、PreCommit(预提交阶段)和DoCommit(正式提交阶段)。同时,协调者和参与者都实现了超时策略:如果等待超过一定时间收不到消息,就自动采取默认动作。这些改进使3PC在理论上避免了2PC中协调者挂掉后参与者永远锁资源的问题。

3PC工作机制与示例

3PC的完整流程分三步:

  • 阶段一:CanCommit(询问准备)。协调者向参与者发送CanCommit请求,询问是否可以准备提交事务。参与者收到后,尝试执行本地事务检查,并给出初步响应:“Yes,我可以准备提交”或者“No,我无法完成”。但与2PC不同的是,此时参与者若答复Yes不会直接进入等待锁定状态,而是进入一种预备状态,但尚未真正执行事务操作。这个阶段类似2PC的投票阶段。
  • 阶段二:PreCommit(预提交)。如果阶段一所有参与者都响应Yes,协调者就进入预提交阶段,向各参与者发送PreCommit请求。参与者接到PreCommit后,这次会真正执行本地事务操作(如数据库写入),但暂时不做最终提交,而是将操作结果和undo/redo日志持久化,随后反馈一个ACK给协调者,表示“我已成功预提交,随时可以最终提交”。此时参与者已经执行了事务但保持着可提交的中间状态。如果在阶段一有任何一个No或超时未响应,协调者则发送中止(abort)请求给所有参与者,让它们中断事务[2]——此时与2PC类似,所有参与者会回滚/取消操作,各自释放资源,然后结束事务。
  • 阶段三:DoCommit(提交或中止)。在预提交成功后,协调者收到所有参与者的ACK确认,进入最后阶段。正常情况下,协调者发送doCommit请求给参与者,通知正式提交。参与者收到后,因为之前已经执行过事务操作了,现在只需完成最终的提交(例如把之前写入的记录正式提交事务)并释放资源,然后向协调者发送确认提交的ACK。协调者收到所有ACK后,事务完成。如果在预提交阶段协调者检测到有参与者失败或超时未反馈,则会发送abort请求,通知所有参与者回滚已做的操作(基于undo日志撤销变更)。

这样3PC确保即使在最后阶段需要中止,也有之前的预提交日志可用于回滚,尽量保证所有节点一致行动。 3PC通过 “提前加一道预提交确认” 来降低不确定性:在最终提交之前,各参与者已经有机会先执行事务、写好日志,并且只有在都成功预执行的前提下才进入真正提交阶段。这样如果协调者在最终阶段突然故障,参与者等一段超时后会选择自动提交,因为它们知道所有人都已预提交且没人否决,所以成功的概率很大。相比之下,在2PC中如果协调者挂了,参与者只能被动等待,无法擅作主张。3PC通过超时策略使参与者不会无限等待——如果等不到最终指令,默认提交事务,从而避免长时间锁定资源。

示例场景:仍以前述账户A向B转账100元为例。3PC下,流程如下:协调者首先询问A服务和B服务“可以执行转账吗?” —— A服务检查余额、B服务准备接受,二者都回复“Yes,可以”(阶段一)。接着协调者发出预提交请求,A服务冻结扣款100元(比如标记冻结但未真正扣除),B服务暂记增加100元但未真正对外可用,两个服务都把这些变化记录在各自的事务日志里,然后回复“预提交完成”(阶段二)。现在如果协调者挂了,A和B不会一直等,而是在超时时间后各自自动提交:A正式扣减余额100元,B正式增加100元,使转账完成。这种情况下系统仍达到一致状态(都提交)。当然,如果在第二阶段有任何问题,比如A预执行失败,则协调者会发出中止请求,让B也中止(撤销暂记的100元),实现一致回滚。

3PC听起来解决了2PC的阻塞问题,但它也有局限。首先,它假定网络不会发生严重分区——通过超时机制“认为”缺少消息就是某种故障的信号。但在网络分区的极端情况下,可能出现数据不一致:比如协调者在预提交后决定中止事务,但由于网络问题,有的参与者没收到中止通知,等待超时后反而自行提交了。这样就出现部分节点提交部分节点回滚的不一致局面。因此3PC并不能100%避免不一致,只是在更多故障场景下提高了成功完成事务的概率。另外3PC协议更加复杂,实现成本更高,而且仍然不能抗衡网络异步模型下共识不可能性的理论限制。事实上工业界很少直接实现3PC协议,因为如果需要更高可靠性,通常会选择像Paxos、Raft这样专门的共识算法,或采用补偿事务方案而非让事务长期阻塞。

3PC主要实现方案

3PC更多是学术上讨论的协议,纯粹采用3PC的商用实现并不多见。目前主流的数据库和中间件一般还是用2PC或其变种来实现分布式事务,而没有采用3PC。这是因为3PC对网络环境要求苛刻(例如需要能可靠地检测超时和故障),在现实互联网环境下难以完全保证安全性。多数情况下,如果对可用性要求极高,会通过引入共识算法(如Raft)来避免单点,而不是用3PC这种偏理论的协议。

不过,一些开源分布式事务框架有借鉴3PC的思路。例如早期的分布式系统研究中有使用3PC改进阻塞的问题,但最终还是发现共识算法更可靠。因此目前几乎没有主流框架标称自己实现了3PC协议。可以认为3PC是2PC的改进版本,但在工程上并未广泛落地。分布式事务框架 Seata 没有提供3PC模式(提供的是2PC/AT和TCC、Saga等),国内的DTM框架也未提供3PC模式,可见其实用性不高。

总结来说,3PC提供了一些宝贵思想:如引入超时和预提交来减少阻塞。不过在实际系统里,往往通过业务补偿和幂等性设计来实现最终一致性,因为等待超时自动提交在很多场景并不可靠(尤其可能违背业务逻辑)。因此,业界后续更倾向于TCC、Saga这类业务层协议来解决分布式事务的问题。

TCC(Try-Confirm-Cancel)

TCC起源与解决的问题

TCC 全称为 Try-Confirm-Cancel,是一种应用层面的分布式事务方案。它源于电子商务和金融等业务对高性能事务的需求,是对2PC的业务逻辑层重实现:把原本由事务协调器完成的提交/回滚控制下放到业务服务,由开发者提供每个操作的“预留”“确认”和“取消”三个接口来完成。同样是两阶段提交的思想,但 TCC 不依赖底层数据库锁和XA协议,而是通过业务代码来保证一致性。

TCC 由美国剑桥科技集团于2007年前后提出,后来在国内由支付宝等公司广泛实践,解决的是高并发下分布式事务的性能和可用性问题。2PC 的缺点在于强阻塞、对协调者依赖强,无法满足互联网场景的高吞吐和高可用。TCC 通过让各服务预留资源,再二次确认或取消,避免了长时间分布式锁定,同时也去除了中心化的单点协调者。它适合那些跨多个服务的短周期业务,比如下订单、支付扣款等,需要严格一致但又追求响应速度的场景。TCC 的核心思想是:“凡是一个操作,都需要有对应的确认操作和补偿操作”。Try 阶段相当于对资源“做记号”或“预留”,Confirm 阶段真正执行,Cancel 阶段在有需要时撤销预留,从而实现最终一致性。

TCC工作机制与示例

TCC 把一次业务事务拆成 两阶段三步:第一阶段只有Try,第二阶段根据情况执行Confirm或Cancel。

  • Try阶段:也叫资源预留阶段。各服务执行业务检测和资源预留操作,相当于做好可以提交的准备。例如下订单场景,在Try阶段不直接扣减库存,而只是锁定库存(标记这些库存被预留,不再出售),同时预扣客户余额或冻结相应额度,但不真正扣款。这一步确保后续操作需要的资源都“备好了”。
  • Confirm阶段:如果所有参与服务的Try步骤都成功完成,那么进入Confirm阶段,各服务执行确认操作,真正将之前预留的资源进行提交。比如库存服务在Confirm时真正减掉库存数量并解除锁定状态,支付服务真正扣除冻结的余额。这一步完成后,业务上的修改就生效了。
  • Cancel阶段:如果在Try阶段有任何一个服务失败,或者某个服务的Try超时无法确定成功,那么整个事务进入Cancel阶段。协调方会通知所有已经完成Try的服务执行取消操作,把先前预留的资源释放掉或恢复到初始状态。比如如果订单服务Try阶段发现订单校验失败,需要取消,则库存服务会收到Cancel通知,将预留的库存释放回可售状态,支付服务解冻之前冻结的金额等,确保之前的预留对业务不产生影响。

整个流程类似于2PC,但由业务代码控制而不是数据库事务锁控制。为了保证可靠性,TCC框架通常还要求:空回滚、防悬挂、幂等等机制。例如空回滚是指有可能某服务的Try请求因为网络原因没到达,但Cancel请求先到了,这时Cancel实现必须支持“找不到对应Try记录也要返回成功”,以保证协调者不会一直等待。防悬挂则是防止Cancel比Try先执行的情况:如果Cancel已经把事务标记回滚了,那么后续迟到的Try必须识别这种情况并拒绝执行。幂等则要求Try、Confirm、Cancel三个操作都需要支持重复调用不影响结果,因为网络抖动可能导致重复消息。通过这些手段,TCC 确保无论消息重试、顺序颠倒等异常,都不会破坏最终一致性。

示例场景:以电商下单为例,涉及订单服务、库存服务、支付服务三方。采用TCC流程:

  • Try:订单服务尝试创建订单记录(状态设为“待确认”),库存服务尝试锁定所需商品的库存(不减少,只标记预留),支付服务尝试冻结用户将要支付的金额(不扣款,只冻结)。假设三个服务的Try操作都返回成功,则进入Confirm;如果有一个失败,例如库存不足导致库存服务Try失败,则跳过Confirm直接转Cancel。
  • Confirm:订单服务将订单状态改为“已下单”,库存服务扣减实际库存并解除锁定,支付服务扣除冻结的金额完成扣款。三个服务各自完成确认后,整个事务提交完毕,用户成功下单支付,库存扣减。
  • Cancel:若前面某一步失败触发Cancel,则订单服务删除“待确认”订单或标记订单无效,库存服务释放预留的库存(库存数量恢复可售),支付服务解冻之前冻结的金额。Cancel执行后,各服务的状态回到事务开始前,保证没有库存少了钱却没扣款之类的不一致情况。

可以看到,TCC通过显式的业务补偿逻辑来达成与2PC类似的“全成功或全撤销”效果。它的优势在于减少锁定范围和时间:比如库存在Try阶段可能仅锁定单个商品的库存,而不像2PC那样锁整个事务的资源直到提交。而且Confirm通常可以快速完成(因为已经预留好了,要做的只是final的一步),因此系统整体性能比2PC提高。另外,由于是业务发起并掌控事务,没有单点协调者瓶颈,可靠性也增强。实际上,只要业务设计得当,TCC可以做到事务最终一定完成确认或者取消,达到与XA事务相当的最终一致性,但以更高性能和可用性实现。

当然,TCC的代价是对业务代码入侵较深,需要开发者为每个关键操作编写Try、Confirm、Cancel三个接口,实现成本较高。业务耦合度也大,事务逻辑需要散落在各服务的实现中。此外,在设计补偿逻辑时也要小心,有些操作可能无法简单地补偿(例如发送短信通知用户,在Cancel时无法“收回”短信,只能再发一条说明撤销,这对用户体验不好)。因此TCC适用于操作定义清晰、补偿容易、数据一致性要求严格的场景,比如互联网金融的交易、支付、账务等核心业务,它们往往能为每一步操作定义逆操作且需要强一致性。对于那些长时间的流程或补偿复杂的业务,TCC可能并非最佳选择。

TCC主流实现方案

TCC作为一种设计模式,已经在不少分布式事务框架中得到支持:

  • Seata TCC:Seata框架提供了开箱可用的TCC模式支持。开发者只需定义好Try、Confirm、Cancel三个方法,并通过Seata的注解或API将其注册为TCC事务分支,Seata的事务协调器(TC)会在全局事务中协调调用Confirm或Cancel。Seata TCC模式屏蔽了一部分底层通信细节,让 Java 应用方便地实现 TCC。
  • DTM:DTM(Distributed Transaction Manager)是国内开源的跨语言分布式事务管理器(Go语言实现)。它支持包括TCC在内的多种事务模式。使用DTM时,各服务通过HTTP/gRPC暴露Try/Confirm/Cancel接口,DTM服务器作为协调者按TCC流程调度调用。DTM还提供了子事务屏障等机制帮助开发者处理幂等和防悬挂问题,降低TCC开发难度。
  • Hmily:Hmily(Hi my life is yours)是一个Java实现的分布式事务框架,也支持TCC模式。它采用字节码增强的方式自动生成Confirm/Cancel调用,提供了Spring Boot Starter,方便集成。在支付、电商场景有一些应用。
  • 其他实现:早期有Alibaba的XTS(扩展事务服务)支持TCC,后来统一到Seata。还有一些社区项目如TCC-Transaction(当当网开源)等实现了TCC模式。在ServiceComb Saga、Apache ShardingSphere等分布式框架中,有的也提供TCC类似的功能或扩展。总的来说,TCC概念简单但实现细节繁琐,各家公司通常在自家业务里定制,如果想快速使用,可以选上述成熟框架。

Saga(长事务补偿模式)

Saga起源与解决的问题

Saga 模式源自1987年由 Hector Garcia-Molina 等人在论文中提出的概念。最初Saga是为了解决数据库中长时间运行事务(long-lived transaction)的问题:一个包含许多步骤的事务如果长时间锁定资源,会严重影响并发,于是提出将长事务拆成一系列短事务来执行,中间的每一步都独立提交,如果某一步失败就执行之前已完成步骤的补偿操作来撤销。这个思想在单体数据库中提出,但非常契合微服务架构下跨多个服务的业务一致性挑战。因此,在微服务领域Saga模式被广泛采纳,作为实现最终一致性的一种重要手段。 Saga的核心在于拆分和补偿:把一个全局事务拆解为一系列有序的本地事务(T1, T2, ..., Tn),每个本地事务完成自己的工作并提交;同时为每个本地事务预定义一个补偿事务(C1, C2, ..., Cn),用于在需要时撤销该步骤。如果所有子事务都成功,那很好,整个Saga完成;一旦某一步Ti失败,Saga就按照逆序调用T(i-1), T(i-2)...对应的补偿操作Ci-1, Ci-2...逐步回滚之前已经完成的步骤。最终使得整个系统回到事务开始前的状态。这种“一退到底”的补偿被称作向后恢复(backward recovery)。另外Saga还有一种向前恢复策略,即在某步失败时尝试重试而不是直接回滚,假设每个子事务最终都会成功用于那些必须成功的业务(无限重试直到成功)。

Saga模式避免了跨服务的同步锁,因为每个子事务都是独立提交的,不需要像2PC那样锁住资源直到整个事务结束。这大大提高了并发能力和性能。但代价是Saga只能保证最终一致性,而无法提供隔离性:在Saga执行过程中,前面子事务的结果已经提交,对外界可见,可能被并发事务读到,从而产生中间不一致的情况(脏读、幻读等)[3]。解决办法通常是在业务层做好防护,比如加业务锁或校验,或者接受这些暂时的不一致并通过补偿在事后纠正。这是Saga与2PC/XA那种强一致事务的根本区别。

Saga工作机制与示例

Saga执行流程可以由中心协调或分布式触发两种方式实现:

  • 命令式协调(Orchestration):通过一个中央Saga协调器(也称编排器)来按顺序调用每个服务的动作。协调器事先定义好整个 Saga 包含哪些步骤、顺序如何。如果某一步失败,由协调器负责按照逆序调用相应服务的补偿操作来回滚。这种模式下服务间依赖简单,每个参与者只需听从协调器命令执行操作或补偿,逻辑集中在协调器处[4]。它的优点是实现简单、流程清晰,新增步骤影响局部,不会造成服务间复杂的相互调用[4][5];缺点是协调器本身可能成为复杂逻辑的聚集地和单点,一旦出问题Saga就无法推进[6][7]。现实中可以通过协调器集群或使用可靠的流程引擎(如状态机/工作流引擎)来提高其可用性。
  • 事件式编排(Choreography):没有中央协调者,而是各服务通过事件通知来自行决定下一步。具体做法是第一个服务执行自己的本地事务T1后,发布一个事件通知其他服务;某个服务监听到事件后触发自己的本地事务T2,成功后再发布另一个事件;以此类推,像接力一样让事务在各服务间传播[8]。最终当最后一个服务完成时,它可能发布一个“结束事件”或不再发布事件来终止Saga。这种方式的优点是取消了中心节点,没有单点故障[9][10],而且对于步骤较少的事务流程,非常自然简单,每个服务只关注接收和发送事件即可[11]。然而缺点在于可控性差:服务间依赖通过事件隐式传递,如果Saga步骤很多,事件流变复杂,出现问题时很难追踪调试[12][13]。另外服务之间可能形成循环依赖或竞争,从架构上需要设计好事件命名和订阅关系才能避免混乱。

无论哪种方式,Saga都要求每个子事务对应的补偿操作要幂等、无副作用,即补偿多次执行结果也一样,以应对可能的重复或部分失败情况。同时,由于Saga各子事务是依次提交,隔离性无法保证:如果多个Saga同时修改同一资源,可能互相干扰。所以通常业务上会防止这种情况,比如对关键资源串行化处理或者锁定。例如一个Saga正在处理订单1001的库存扣减,则可能对订单1001加一个业务锁,防止并发的Saga也动它。这些属于Saga并发控制的范畴。

示例场景:以旅行预订为例,需要订航班、订酒店、扣款三个步骤,涉及机票服务、酒店服务、支付服务三个微服务。用Saga编排方式:

  1. 正常执行:Saga协调器按照顺序调用:
  2. 调用航班服务预订航班座位(事务T1),成功则继续
  3. 调用酒店服务预订房间(事务T2),成功则继续
  4. 调用支付服务扣款(事务T3),成功则Saga完成 这三个步骤各自是独立提交的本地事务,最终预订成功则旅客拿到机票和酒店确认,支付扣款完成。
  5. 出现失败:假如在支付服务扣款(T3)这一步信用卡扣款失败,则Saga协调器检测到失败,开始补偿流程:
  6. 按照逆序调用酒店服务的补偿操作C2,取消之前预订的房间;
  7. 调用航班服务的补偿操作C1,取消之前预订的机票; 通过C2和C1的执行,撤销了已经完成的航班和酒店预订,使系统状态回滚到什么都没发生过。用户会收到通知预订未成功,已无需支付。
  8. 事件驱动实现:若用事件编排,则可能是:
  9. 用户触发下单后,首先航班服务收到事件“开始预订行程”,执行T1并发布事件“航班已预订”[8]。
  10. 酒店服务监听到“航班已预订”事件,执行T2预订酒店并发布“酒店已预订”事件[8]。
  11. 支付服务监听到“酒店已预订”事件,执行T3扣款并发布“支付已完成”事件[8]。
  12. 用户服务监听到“支付已完成”事件,通知用户预订成功。

如果支付失败,在事件模式下可能需要支付服务发布一个“支付失败”事件,航班服务和酒店服务订阅该事件后各自执行取消预订的操作(C1、C2)。这种补偿的触发和执行都是分散的,需要设计好事件流。由于示例中发送了用户通知短信,补偿时还可能需要再发一条通知说明预订取消(这正是Saga补偿在某些业务上的麻烦之处)。

Saga模式的优缺点可以从上述过程看出:它避免了长时间锁,每步都立即提交,系统整体吞吐量较高;但中间状态外露,需要接受一定的脏读、重复更新等情况,并通过补偿和业务逻辑解决[3]。如果补偿动作容易定义(比如撤销库存、退款等相对简单),Saga就很好用;但如果补偿难以处理或者用户对过程中状态不一致很敏感,Saga就需要仔细斟酌。例如提到发送短信的场景,用Saga意味着可能发两次短信通知和撤销,用户体验变差,所以不一定合适这种场景。

Saga主流实现方案

由于Saga属于模式范畴,各家公司和社区都提供了不少实现:

  • Seata Saga:Seata 通过状态机方式实现了Saga模式。开发者可以用 JSON 或图形化定义 Saga 的执行步骤和补偿步骤,Seata 的 Saga 引擎会负责按定义执行和回滚。Seata Saga 模式适合 Java 微服务,已用于订单、电商等领域。
  • Apache ServiceComb Saga:Apache ServiceComb 项目提供了 Saga 协调器,支持以注解方式定义事务和补偿,并通过Alpha协调器管理Saga流程。它在ServiceComb微服务框架里用于保证跨服务的数据一致性。
  • Workflow引擎:利用通用的工作流/编排引擎也是实现Saga的常见方式。例如 AWS Step Functions、Azure Durable Functions、Netflix Conductor、Temporal 等,都可以用状态机或流程图定义Saga步骤,发生错误时由流程引擎调度补偿步骤。这类方案往往提供高可用的协调服务,避免单点问题[14]。比如 AWS Step Functions 天然支持Saga的补偿模式,可确保即使某台协调器宕机,流程也不会丢。
  • Axon Framework:Axon是CQRS和事件溯源架构的Java框架,其中Saga是重要概念。Axon Saga更多采用事件驱动方式:Saga监听一系列领域事件,根据事件进展来调用不同服务,维护Saga状态机。Axon Saga常用于DDD领域驱动设计的复杂业务流程管理。
  • 自定义实现:很多团队根据业务需要自己实现Saga协调。比如简单场景下,可以自己写一个Saga管理服务,它记录每个子事务状态,调用各服务接口并记录需要补偿的操作,当失败时按照记录调用补偿。或者采用消息队列+状态追踪的方式实现事件编排的Saga。自实现需要考虑幂等和错漏处理,但对特定业务可做定制优化。

总之,Saga 已成为微服务一致性方案的标配之一。当无法使用强一致的分布式事务时,Saga通过最终一致性加补偿提供了一条务实可行的道路。在电商订单、库存、支付、账户等跨系统交易中非常普遍。

Outbox 本地事务消息模式

Outbox起源与解决的问题

Outbox模式(又称事务消息发件箱模式)是一种分布式一致性设计模式,专门用来解决数据库操作和消息通知的双重写入问题。最初由 eBay 架构师 Dan Pritchett 于2008年在 ACM 上发表文章提出[15][16]。在微服务中,经常出现这样的场景:一个服务需要更新自己的数据库并向消息队列(或事件流)发送一条消息通知其他服务。例如订单服务在写入订单数据库记录后,需要发送一条“订单已创建”的事件到消息系统,让库存服务、物流服务知道发生了新订单。问题在于:数据库写和发送消息是两个独立操作,如果不加控制,可能出现部分成功的情况——比如数据库写成功了但消息发送失败,那么其他服务蒙在鼓里不知数据库其实已经变动;反之,消息发出去了但数据库写入却回滚了,其他服务收到消息却发现数据不存在。这就是所谓“双写不一致”问题。

传统上可以用2PC来让数据库和MQ都参与一个全局事务,但许多消息系统不支持XA事务,而且这么做会牺牲性能。Outbox模式提供了一个巧妙且可靠的方案:利用本地事务保证写数据库和记录消息的原子性,然后异步将记录的消息发送出去。其核心思想是:把要发送的消息存储在发送方自己的数据库里,作为一张“Outbox”消息表,和业务数据的更新在同一个本地事务中完成。这样,就保证了业务数据和消息记录的要么一起提交、要么一起回滚。随后,一个独立的消息中转组件从Outbox表读取新插入的消息,并将它们投递到真正的消息系统(比如Kafka、RabbitMQ)或直接调用下游服务接口。只要确保这个中转发送机制可靠运行,最终所有写入Outbox的消息都会送达,发送失败的可以重试。而如果业务事务回滚了,则Outbox里根本不会留下记录,因此也就不会发送错误的消息。简而言之:Outbox模式将跨系统的分布式事务拆解成发送方本地的事务 + 异步消息投递,保证了一致性的同时降低了耦合和锁等待。

Outbox工作机制与示例

Outbox模式通常包含以下步骤:

  1. 在需要发送通知的服务的数据库中,建立一张消息表(Outbox表),用于存储要发出的事件/消息。表结构至少包括消息ID、类型、内容、状态、时间戳等字段。
  2. 当该服务在一个业务操作的本地事务中更新其业务数据时,同时插入一条消息记录到Outbox表里。这两个写操作(业务表和消息表)放在一个数据库事务中提交。于是保证了:“要么业务数据变更和消息记录都成功提交,要么两者都不执行”。例如订单服务在本地事务里同时完成:“插入订单表新订单记录”以及“插入Outbox表一条‘订单已创建’消息”。
  3. 事务提交后,这条Outbox消息就持久化在数据库里。接下来,一个独立的消息转发进程负责读取Outbox表的新消息并发送到消息中间件或调用下游逻辑。实现方式多种多样,常见的有:
  4. 轮询任务:一个后台线程定期扫描Outbox表状态为“未发送”的记录,取出逐条发送。发送成功则将记录状态改为“已发送”或删除;发送失败则留在表中等待下次重试。这样确保即使第一次发送失败,后续还有机会补发。
  5. 触发器/事件:有的数据库支持触发器或发布变更事件(如Postgres的LISTEN/NOTIFY),可以在事务提交时立即触发一个事件给消息转发逻辑,加快实时性。
  6. 变更数据捕获(CDC):这是更先进的做法,通过读取数据库的事务日志(binlog/redo log),识别Outbox表的新插入记录,实时地将其转换为消息推送出去。工具如Debezium就是这么做的:Debezium连接到数据库,监控Outbox表的插入变更,一旦检测到,就构造对应的事件发送到Kafka等消息系统。这种方式无需轮询,低延迟且对业务零侵入,在业界很流行。
  7. 下游服务通过正常的消息订阅机制(如订阅Kafka topic,或者从MQ队列消费)接收消息,进而执行相应处理(如库存服务收到“订单已创建”事件后扣减库存)。
  8. (可选)发送方在确认消息被下游处理后,可以删除或归档Outbox表中的记录,防止表无限增长。但为了可靠性,通常采用“至少投递一次”策略,可能会保留一段时间允许重复发送。下游要能容忍重复消息(通过消息ID幂等处理)。

示例场景:订单服务和库存服务解耦。过去订单服务下单后需要调用库存服务扣库存,如果调用失败要回滚订单,很复杂。现在用Outbox模式,订单服务下单时在一个本地事务中:(a) 写入订单表新订单状态=“已创建”; (b) 写入Outbox表一条消息 {type:"OrderCreated", orderId:1234, content: {...}}。如果这事务成功提交,则订单库有了新订单、Outbox有待发送消息。如果事务失败(比如订单数据校验失败),则都不会写入,库存服务也就不会收到任何错误通知。

事务提交后,订单服务的一个后台进程或Debezium捕获到Outbox出现了新的“OrderCreated”事件。它立即将此事件发送到如Kafka的“OrderEvents”主题。库存服务订阅该主题,拿到订单事件后,进行库存扣减操作并记录处理成功。关键点在于:如果订单写入成功,则肯定有事件;如果订单写入不成功,就没有事件。不会出现订单库改了但库存服务不知道,或者库存收到一个订单通知但订单库里却没有记录的尴尬情况。两边的数据是一致的。哪怕发送Kafka的过程出现问题,比如Kafka临时故障,Outbox消息还躺在订单库里,下次重试或故障恢复后仍会发送。就算订单服务整个挂了,Outbox消息仍在数据库,恢复后依然可以继续发送,不会丢消息。这就大大提高了跨服务通信的可靠性。

Outbox模式实际上把分布式事务变成了本地事务 + 异步最终一致性。它适用于对数据一致性要求高,但允许短暂延迟的场景。比如用户注册后需要发送欢迎邮件,可以用Outbox保证注册信息落库和待发邮件记录同时成功,然后异步发邮件;订单支付后需要通知发货,也可用Outbox保障支付记录与通知一致。不适用于要求强实时同步的操作,因为消息是异步的,但大部分业务通知都能容忍秒级的延迟。

Outbox主要实现方案

Outbox模式本身是一种设计思路,各种技术栈都可以实现。下面介绍几种常见的实现方式或工具:

  • Debezium + Kafka(CDC方案):Debezium是知名的开源CDC平台。它可以监控数据库表的变化,尤其和Outbox模式结合紧密。Debezium有一套Outbox Event Router插件:当Outbox表有新记录时,自动读取其内容并转换成消息发送到Kafka指定主题。很多使用Kafka做事件总线的系统都采用这一方案。例如在Quarkus框架中,有现成的Debezium Outbox扩展来简化配置。这种方案优点是实时、可靠、免轮询,而且Kafka天然具备消息持久化和广播能力,方便扩展多个下游订阅者。
  • 本地轮询发送:一些简单场景下,可以自己实现一个消息发送调度。比如每隔若干秒扫描消息表,发送消息。可以用数据库的标志位或状态字段来记录是否已发送成功。如果发送失败,可稍后重试。为了避免频繁扫描整个表,可以结合时间戳或消息状态索引提高效率。这种方式实现简单,但延迟取决于轮询频率,频繁轮询又怕给数据库增加负担,所以一般折中为数秒级别扫描一次。
  • RocketMQ事务消息:这是另外一种解决方案,和Outbox思路相近但实现层级不同。RocketMQ在4.3版本后支持事务消息,允许发送方先向MQ发送半消息(预消息),MQ持久化后通知发送方,然后发送方执行本地事务(比如写数据库),最后根据事务结果反馈给MQ提交或回滚消息。如果本地事务成功则让MQ将消息投递出去,如果失败则MQ丢弃消息。即使发送方崩溃没反馈,MQ也会通过回查机制询问发送方事务状态,决定提交还是删除消息。RocketMQ的这种“两阶段消息”本质上等价于把Outbox表和轮询的逻辑内置在MQ中:MQ充当了消息表的存储和检查者。这种方式减少了自己处理Outbox的工作量,但锁定在特定MQ产品上,而且需要实现事务回查接口。使用上RocketMQ事务消息较适合对一致性要求中等、能够实现回查逻辑的业务。
  • Dapr Binding/Workflow:对于Polyglot环境,微软的Dapr微服务框架提供了一种Outbox支持:应用通过Dapr的Output Binding把事件写入一个事务表,然后Dapr组件负责轮询和投递。这类似于把Outbox模式封装成一个可复用部件,开发者无需亲自处理表和轮询。
  • DTM Outbox模式:前面提到的 DTM 框架也支持Outbox模式(称为“事务消息”模式)。不过DTM选择实现了RocketMQ式的事务消息,而未直接实现本地消息表,因为相比之下事务消息封装更好、开发更简单。DTM的事务消息通过HTTP接口提供类似半消息提交、本地事务执行、结果通知的流程,使用起来跟Outbox思想一致但封装在框架中。

需要注意的是,Outbox模式一般保证消息至少投递一次,可能出现重复消息(例如发送方重启后再次发送尚未标记发送的记录)。因此下游消费者应设计为幂等处理,同一消息多次处理不会出错[17]。常见做法是在消息里包含唯一ID,消费者维护一个已处理ID列表,不重复处理。同样,Outbox表需要定期清理或归档,否则长期积累会膨胀。可以在确认消息送达后删记录,或者把已发送且过期的记录转移走。总的来说,Outbox模式将复杂的分布式事务问题转化为了本地事务 + 异步可靠消息问题,利用数据库的ACID性质和消息系统的最终一致性,取得了一个性能与一致性的折中,因此被广泛认为是微服务数据一致性领域的实用方案。

各方案对比总结

以上介绍了五种主要的分布式事务方案,它们在一致性保证、复杂度和性能上各有特点。下面将它们的优缺点和适用场景做一个简要对比:

方案 优点 缺点 适用场景
2PC (XA) 强一致性和原子性,所有节点要么全提交通用,要么全回滚;实现对开发透明,多由数据库中间件完成。 性能差:需要协调两阶段,多次通信且事务期间资源阻塞;协调者单点故障可能导致事务挂起;网络故障可致部分提交不一致。 传统强一致性要求场景,如单体应用跨多个数据库操作;节点较少、网络可靠的环境。例如企业内部应用、银行核心账务在小范围内使用XA事务。
3PC 相比2PC降低了阻塞风险:引入超时机制,协调者出故障参与者可自决,不会无限等待;在进入最终提交前确保参与者状态一致。 协议复杂,实现少见;仍无法覆盖网络分区等极端情况,可能出现数据不一致;增加一个通信阶段也增加延迟。 理论改进方案,实际很少直接使用。可用于特殊容错要求的场景作为学术探索。但工业界更倾向使用共识算法或其他机制来避免阻塞。
TCC 本地事务拆分业务控制,性能高于2PC:锁定粒度小、锁定时间短;无中央协调单点,协调由业务方或框架集群完成;最终一致性有保障,Confirm和Cancel幂等重试确保事务要么确认要么取消。 开发工作量大,需为每个操作编写Try/Confirm/Cancel逻辑,业务侵入强;要求服务实现幂等、支持空回滚等,会增加复杂性;不适合长事务,预留资源占用过久可能复杂。 高实时、高并发场景需要分布式事务时,比如电商订单、支付等短平快操作;尤其适合金融交易等小范围事务,一次事务涉及的服务和步骤有限,且可明确定义补偿。
Saga 业务流程松耦合,无全局锁,每步立即提交,提升系统并发性能;最终一致性,通过补偿确保失败可回滚;实现灵活:可中心编排易管理或事件驱动易扩展;适合长生命周期事务,允许手工干预和检查。 只保证最终一致而非瞬时一致,中间状态对外可见,需容忍短暂不一致;隔离性弱并可能引发并发更新冲突,需业务方额外控制(如加锁或防重);补偿逻辑不总是好写,某些操作无法完全逆转(例如外部通知等需要特别处理);如果步骤非常多,事件编排模式下调试复杂。 跨多个服务的业务流程,允许最终一致性即可,如订单流程、用户注册流程等;长事务(如跨天的流程)不能用数据库锁的,只能用Saga这种补偿方式;微服务架构中常用,特别适合参与者较多但每步易补偿的业务。反之,不适合高并发修改同一数据的场景,因为并发下Saga缺少隔离会麻烦。
Outbox 模式 保证本地数据变更与消息发送的原子性,不需要分布式锁定;实现简单可靠,利用数据库事务和消息重试机制确保消息“不丢不重”发送;异步解耦,发送方事务提交后即可返回,提高吞吐;与消息队列/事件流自然集成,可扩展通知给多个下游。 属于最终一致方案,消息有微小延迟下游才能收到;Outbox表引入运维开销:需要后台进程/CDC来监控和发送消息,Outbox数据需要清理;实现需注意防止重复消息、保证消费者幂等;不直接解决多资源更新,只解决单服务内数据库+消息一致性问题,需要配合Saga/TCC用于更复杂事务。 事件驱动微服务中非常常用:如下单后通知库存/物流、用户操作后发送邮件短信等;适用于对数据一致性要求高但允许异步通知的场景;也常用于集成数据库和Kafka、数据库和缓存等需要双写的情况。Outbox模式尤其在CQRS/Event Sourcing架构中用于事件发布,或替代跨服务强一致要求的简单场景(比如只涉及通知)。

注:以上对比中,2PC/3PC提供强一致性(满足ACID,但降低可用性);TCC/Saga/Outbox提供最终一致性(满足BASE理念,追求可用性和性能),需要业务能接受短暂不一致并配合补偿机制。选择方案时应根据业务对一致性与性能的权衡,以及参与事务的系统数量和操作复杂度来决定。例如,高价值的银行转账可能用TCC明确控控美元去留,而用户积分变更这种可以稍后纠正的可用Saga或Outbox。

最后,要强调在实际项目中,没有银弹方案。复杂的分布式事务往往采用组合策略:局部用XA两阶段提交,跨服务用TCC或Saga,配合Outbox确保消息通知可靠等等。例如,阿里巴巴的Seata框架就提供了多种模式,方便开发者按需选用;国内的DTM框架也支持 TCC、Saga、XA、事务消息/Outbox 等模式。工程实践中,应充分考虑系统的一致性要求、性能要求、失败补偿成本来选取合适方案,并遵循设计原则如避免长时间锁、幂等设计、失败检测与告警等,来构建健壮的分布式事务处理机制。通过对比各方案的特点,希望读者对分布式事务有了全局认识,能够在具体场景下快速上手并做出合理选择。每种方案都有成功的应用案例,理解其机制后加以组合,才能既保障数据一致,又兼顾系统的伸缩性与健壮性。


[1] 分布式的共识:原子提交和两阶段提交
https://www.nebula-graph.com.cn/posts/atomic-Zookeeper

[2][3][4][5][6][7][8][9][10][11][12][13][18] 七种常见分布式事务详解(2PC、3PC、TCC、Saga、本地事务表、MQ事务消息、最大努力通知) - CSDN博客
https://blog.csdn.net/a745233700/article/details/122402303

[14] Saga 编排模式 - AWS 规范性指导
https://docs.aws.amazon.com/zh_cn/prescriptive-guidance/latest/cloud-design-patterns/saga-orchestration.html

[15][16] 其他事务模式 | DTM开源项目文档
https://dtm.pub/practice/other

[17] 事务发件箱模式 - AWS 规范性指导
https://docs.aws.amazon.com/zh_cn/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html