事务的转变过程
单点项目 - 单数据源事务

在单体项目中如果只有一个数据库是不存在分布式事务问题的,
通过@Transactional事务管理器就可以管理单数据库事务
单点项目 - 多数据源事务

单体项目中如果有多个数据源时,每个数据源都有自己独立的事务管理器,所以会存在事务问题(同一个方法中调用到了两个数据源)
轻量级解决方案:使用JTA+Atomikos解决单体项目多数据源事务
微服务项目事务

在微服务项目中,每个微服务应用都可能会有自己的数据库, @Transactional 是无法跨服务的,就会存在分布式事务的问题。
在微服务环境下,因为会根据不同的业务会拆分成不同的服务,比如会员服务、订单服务、积分服务等,让专业的人做专业的事情,每个服务都有自己独立的数据库,并且是独立运行,互不影响。服务与服务之间通讯采用RPC远程调用技术,但是每个服务中都有自己独立的数据源,即自己独立的本地事务。两个服务相互通讯的时候,两个本地事务互不影响,从而出现分布式事务产生的原因。
事务理论知识
ACID理论
数据库管理系统中事务(transaction)的四个特性(分析时根据首字母缩写依次解释):
- 原子性(Atomicity)
原子性是指事务是一个不可再分割的工作单元,事务中的操作要么都发生,要么都不发生
- 一致性(Consistency)
一致性是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性
- 隔离性(Isolation)
多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。
- 持久性(Durability)
这是最好理解的一个特性:持久性,意味着在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。(完成的事务是系统永久的部分,对系统的影响是永久性的,该修改即使出现致命的系统故障也将一直保持)
CAP理论
由于对系统或者数据进行了拆分,我们的系统不再是单机系统,而是分布式系统,针对分布式系统的CAP理论包含如下三个元素:
C:一致性(Consistency)
一致性是指,一旦客户端将值写入任何一台服务器并获得响应,那么之后任意客户端从任意服务器读取的都是刚刚新写入的数据。

一致性保证了:不管向哪台服务器写入数据,其他的服务器能实时同步这个新数据,之后不管从哪个服务器读取数据得到的都是最新的数据。如果同步有问题,那么首先解决同步的问题,挂起请求等待同步完成后才响应,那么可能是延迟很久才得到了正确结果,也可能是请求异常或者结果异常。

A:可用性(Availability)
可用性是指,向任意未崩溃的服务器发送请求,总能保证收到响应数据(允许不是最新数据)

P: 分区容错性(Partition tolerance)
分区的概念是:在分布式系统中,不同的节点分布在不同的子网络中,由于一些特殊的原因,这些子节点之间出现了网络不通的状态,但他们的内部子网络是正常的。从而导致了整个系统的环境被切分成了若干个孤立的区域。这就是分区。
分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务。比如两个服务器,给对方任何消息都可以失败;也就是说会因为各种意外情况,导致无法成功进行数据同步,分布式系统要能容忍这种情况。

CAP的取舍
传统的单机系统满足CA,而没有P。在分布式系统中,首先必须要满足P(分布式系统,因为是多节点的,一定要考虑part failure),所以C,A需要根据具体场景进行取舍。
通过一个示例来梳理,如整个系统由服务节点A、B组成,之间通过网络通信,当节点 A 进行更新数据操作的时候,需要同时更新节点 B 的数据(这个数据复制的过程是一个原子操作)。

上图这个系统怎么满足 CAP 呢?通过反证法来梳理: 假设可以同时满足一致性、可用性、分区容错这三个特性,由于满足分区容错,可以切断 A、B两个服务节点间的通信
- A、B连接断开后,就出现了网络分区,因为节点 A、B无法通信,一致性就无法满足。
- 如果强行满足一致性,就必须停止提供服务,修复A、B的通信故障,从而放弃可用性。
总结来看:
- 若要保证一致性:则必须进行节点间数据复制,复制期间数据锁定,导致期间的读取失败或超时,破坏了可用性;
- 若要保证可用性:则不允许节点间数据复制时被锁定,这又破坏了一致性。
| 组合 | 结果 |
|---|---|
| AC | 单机应用 满足一致性和可用性,没有分区容错 |
| CP | 满足一致性和分区容错性,也就是说,要放弃可用性。当系统被分区,为了保证原子性,必须放弃可用性,让服务停用。 |
| AP | 满足可用性和分区容错性,当出现分区,同时为了保证可用性,即使数据未同步,也必须让节点继续对外服务,这样必然导致失去数据的一致性。 |

CAP原理指出这三个要素最多只能同时实现两点,不可能三者兼顾。
BASE理论
因此在进行分布式架构设计时,必须做出取舍。而对于分布式数据系统,分区容忍性是基本要求,否则就失去了价值。因此设计分布式数据系统,就是在一致性和可用性之间取一个平衡。对于大多数web应用,其实并不需要强一致性,因此牺牲一致性而换取高可用性,是目前多数分布式产品的方向。
当然,牺牲一致性,并不是完全不管数据的一致性,否则数据是混乱的,那么系统可用性再高分布式再好也没有了价值。牺牲一致性,只是不再要求关系型数据库中的强一致性,而是只要系统能达到最终一致性即可,考虑到客户体验,这个最终一致的时间窗口,要尽可能的对用户透明,也就是需要保障“用户感知到的一致性”。通常是通过数据的多份异步复制来实现系统的高可用和数据的最终一致性的,“用户感知到的一致性”的时间窗口则取决于数据复制到一致状态的时间
因此,我们退而求其次,在C和A中进行权衡,最终形成了我们的BASE理论:
- Basically Available(基本可用);
- Soft state(软状态);
- Eventually consistent(最终一致性);
BASE定理的核心思想:即使无法做到强一致性,但是每个应用可以根据自身的业务特定,采用合适的方式来达到最终一致性。

Basically Available(基本可用)
- 基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。但是,这绝不等价于系统不可用。
- 损失相应时间:CAP可用性的服务相应时间可能是10ms,而BASE基本可用性的相应时间1-2s,即允许损失部分可用性(时间上的损失);
- 损失系统功能:BASE的基本可用性是允许某个服务出现故障时,采用服务降级等手段保证用户的体验性,即允许损失部分系统功能;
Soft state(软状态)
软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
Eventually consistent(最终一致性)
- 它和强一致性不一样,强一致性读操作要么处于阻塞状态,要么读到的是最新的数据;
- 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
柔性事务和刚性事务
柔性事务满足BASE理论(基本可用,最终一致)
刚性事务满足ACID理论
分布式事务的解决方案
在分布式架构下,每个节点只知晓自己操作的失败或者成功,无法得知其他节点的状态。当一个事务跨多个节点时,为了保持事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚(rollback)。
2阶段提交(2PC)
二阶段提交协议(Two-phase Commit,即 2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理。
两个阶段分别为:
- 准备阶段
- 提交阶段
参与的角色:
- 事务协调者(事务管理器TM):事务的发起者
- 事务参与者(资源管理器RM):事务的执行者
准备阶段(投票阶段)
这是两阶段的第一段,这一阶段只是准备阶段,由事务的协调者发起询问参与者是否可以提交事务,但是这一阶段并未提交事务,流程图如下图:

- 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复
- 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)
- 如参与者执行成功,给协调者反馈同意,否则反馈中止
提交阶段
这一段阶段属于2PC的第二阶段(提交/执行阶段),协调者发起正式提交事务的请求,当所有参与者都回复同意时,则意味着完成事务,流程图如下:

- 协调者节点向所有参与者节点发出正式提交(commit)的请求。
- 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
- 参与者节点向协调者节点发送ack完成消息。
- 协调者节点收到所有参与者节点反馈的ack完成消息后,完成事务。
但是如果任意一个参与者节点在第一阶段返回的消息为终止,或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时,那么这个事务将会被回滚,回滚的流程图如下:

- 协调者节点向所有参与者节点发出回滚操作(rollback)的请求。
- 参与者节点利用阶段1写入的undo信息执行回滚,并释放在整个事务期间内占用的资源。
- 参与者节点向协调者节点发送ack回滚完成消息。
- 协调者节点受到所有参与者节点反馈的ack回滚完成消息后,取消事务。
不管最后结果如何,第二阶段都会结束当前事务。
二阶段提交的事务正常提交的完整流程如下图:

二阶段提交事务回滚的完整流程如下图:

举个百米赛跑的例子来具体描述下2PC的流程:学校运动会,有三个同学,分别是A,B,C,2PC流程如下:
- 裁判:A同学准备好了吗?准备进入第一赛道….
- 裁判:B同学准备好了吗?准备进入第一赛道….
- 裁判:C同学准备好了吗?准备进入第一赛道….
- 如果有任意一个同学没准备好,则裁判下达回滚指令
- 如果裁判收到了所有同学的OK回复,则再次下令跑……
- 裁判:1,2,3 跑…………
- A同学冲刺到终点,汇报给裁判
- B,C同学冲刺失败,汇报给裁判
2PC的缺点
二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:
- 性能问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
- 可靠性问题:参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。
- 数据一致性问题:二阶段无法解决的问题:协调者在发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
- 实现复杂:牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
2PC的优点
- 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)
3阶段提交(3PC)
三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点。
- 在协调者和参与者中都引入超时机制
- 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。处理流程如下:

阶段一:CanCommit阶段
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
- 事务询问:协调者向所有参与者发出包含事务内容的canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
- 响应反馈:参与者收到canCommit请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。
CanCommit阶段流程如下图:

阶段二:PreCommit阶段
协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能。
- 假如所有参与者均反馈 yes,协调者预执行事务。
- 发送预提交请求 :协调者向参与者发送PreCommit请求,并进入准备阶段
- 事务预提交 :参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中(但不提交事务)
- 响应反馈 :如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

- 假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
- 发送中断请求 :协调者向所有参与者发送abort请求。
- 中断事务 :参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

阶段三:doCommit阶段
该阶段进行真正的事务提交,也可以分为以下两种情况。
进入阶段3后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的doCommit请求或abort请求。此时,参与者都会在等待超时之后,继续执行事务提交。
- 执行提交
- 发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
- 事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
- 响应反馈 事务提交完之后,向协调者发送ack响应。
- 完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

- 中断事务:任何一个参与者反馈no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务
- 发送中断请求 如果协调者处于工作状态,向所有参与者发出abort请求
- 事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
- 反馈结果 参与者完成事务回滚之后,向协调者反馈ACK消息
- 中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

3PC优点
相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段3中协调者出现问题时,参与者会继续提交事务。
3PC缺点
数据不一致问题依然存在,当在参与者收到preCommit请求后等待doCommit指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
TCC(事务补偿)
TCC(Try Confirm Cancel)方案是一种应用层面侵入业务的两阶段提交。是目前最火的一种柔性事务方案,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。
TCC分为两个阶段,分别如下:
- 第一阶段:Try(尝试),主要是对业务系统做检测及资源预留 (加锁,锁住资源)
- 第二阶段:本阶段根据第一阶段的结果,决定是执行confirm还是cancel
- Confirm(确认):执行真正的业务(执行业务,释放锁)
- Cancle(取消):是预留资源的取消(出问题,释放锁)

为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。
假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。
阶段一:Try阶段
TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:
- 完成所有业务检查( 一致性 ) 。
- 预留必须业务资源( 准隔离性 ) 。
- Try 尝试执行业务。

阶段二:Confirm/Cancel阶段
根据 Try 阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。
Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。
Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作,业务如下图:

这里使用的资源一定是 Try 阶段预留的业务资源。在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。
Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。
Cancel:当 Try 阶段存在服务执行失败, 进入 Cancel 阶段,业务如下图:

最终一致性保证
- TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
- Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。也就是说只要Try成功,Confirm一定成功(TCC设计之初的定义) 。
- Confirm与Cancel如果失败,由TCC框架进行==重试==补偿
- 存在极低概率在CC环节彻底失败,则需要定时任务或人工介入
TCC优点
TCC 事务机制相对于传统事务机制(X/Open XA),TCC 事务机制相比于上面介绍的 XA 事务机制,有以下优点:
- 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
- 数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
- 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
TCC缺点
- TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
本地消息表
具体方案
本地消息表的方案最初是由 eBay 提出,核心思路是将分布式事务拆分成本地事务进行处理。
角色:
- 事务主动方
- 事务被动方
通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
这样可以避免以下两种情况导致的数据不一致性:
- 业务处理成功、事务消息发送失败
- 业务处理失败、事务消息发送成功
整体的流程如下图:

上图中整体的处理步骤如下:
- ①:事务主动方在同一个本地事务中处理业务和写消息表操作
- ②:事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。
- ③:事务被动方通过消息中间件,通知事务主动方事务已处理的消息。
- ④:事务主动方接收中间件的消息,更新消息表的状态为已处理。
一些必要的容错处理如下:
- 当①处理出错,由于还在事务主动方的本地事务中,直接回滚即可
- 当②、③处理出错,由于事务主动方本地保存了消息,只需要轮询消息重新通过消息中间件发送,事务被动方重新读取消息处理业务即可。
- 如果是业务上处理失败,事务被动方可以发消息给事务主动方回滚事务
- 如果事务被动方已经消费了消息,事务主动方需要回滚事务的话,需要发消息通知事务主动方进行回滚事务。
本地消息表优点
- 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
- 方案轻量,容易实现。
本地消息表缺点
- 与具体的业务场景绑定,耦合性强,不可公用。
- 消息数据与业务数据同库,占用业务系统资源。
- 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。
MQ事务方案(可靠消息事务)
基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。
具体方案
MQ事务方案整体流程和本地消息表的流程很相似,如下图:

从上图可以看出和本地消息表方案唯一不同就是将本地消息表存在了MQ内部,而不是业务数据库中。
那么MQ内部的处理尤为重要,下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。
在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ提供了 2PC 的提交接口,方案如下:
正常情况:事务主动方发消息

这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:
- 步骤①:发送方向 MQ 服务端(MQ Server)发送 half 消息。
- 步骤②:MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。
- 步骤③:发送方开始执行本地事务逻辑。
- 步骤④:发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
- 步骤⑤:MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。
异常情况:事务主动方消息恢复

在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:
- 步骤⑤:MQ Server 对该消息发起消息回查。
- 步骤⑥:发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 步骤⑦:发送方根据检查得到的本地事务的最终状态再次提交二次确认。
- 步骤⑧:MQ Server基于 commit/rollback 对消息进行投递或者删除。
MQ事务方案优点
相比本地消息表方案,MQ 事务方案优点是:
- 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
- 吞吐量大于使用本地消息表方案。
MQ事务方案缺点
- 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
- 业务处理服务需要实现消息状态回查接口。
最大努力通知
最大努力通知也称为定期校对,是对MQ事务方案的进一步优化。它在事务主动方增加了消息校对的接口,如果事务被动方没有接收到消息,此时可以调用事务主动方提供的消息校对的接口主动获取。
最大努力通知的整体流程如下图:

在可靠消息事务中,事务主动方需要将消息发送出去,并且消息接收方成功接收,这种可靠性发送是由事务主动方保证的;
但是最大努力通知,事务主动方尽最大努力(重试,轮询….)将事务发送给事务接收方,但是仍然存在消息接收不到,此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费,这种通知的可靠性是由事务被动方保证的。
最大努力通知适用于业务通知类型,例如微信交易的结果,就是通过最大努力通知方式通知各个商户,既有回调通知,也有交易查询接口。
Saga事务
Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文。
Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
Saga 事务基本协议如下:
- 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。
- 每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。
TCC事务补偿机制有一个预留(Try)动作,相当于先报存一个草稿,然后才提交;Saga事务没有预留动作,直接提交。
对于事务异常,Saga提供了两种恢复策略,分别如下:
向后恢复(backward recovery)
在执行事务失败时,补偿所有已完成的事务,是“一退到底”的方式。如下图:

从上图可知事务执行到了支付事务T3,但是失败了,因此事务回滚需要从C3,C2,C1依次进行回滚补偿。
对应的执行顺序为:T1,T2,T3,C3,C2,C1
这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。
向前恢复(forward recovery)
也称之为:勇往直前,对于执行不通过的事务,会尝试重试事务,这里有一个假设就是每个子事务最终都会成功。 流程如下图:

适用于必须要成功的场景,事务失败了重试,不需要补偿。
Saga事务有两种不同的实现方式,分别如下:
- 命令协调(Order Orchestrator)
- 事件编排(Event Choreographyo)
命令协调
中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。整体流程如下图:

上图步骤如下:
- 事务发起方的主业务逻辑请求 OSO 服务开启订单事务
- OSO 向库存服务请求扣减库存,库存服务回复处理结果。
- OSO 向订单服务请求创建订单,订单服务回复创建结果。
- OSO 向支付服务请求支付,支付服务回复处理结果。
- 主业务逻辑接收并处理 OSO 事务处理结果回复。
中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。
基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。
事件编排
没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。
在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。
当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。

上图步骤如下:
- 事务发起方的主业务逻辑发布开始订单事件。
- 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件。
- 订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件。
- 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件。
- 主业务逻辑监听订单已支付事件并处理。
事件编排是实现Saga模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。
Saga事务优点
命令协调设计的优点如下:
- 服务之间关系简单,避免服务之间的循环依赖关系,因为Saga协调器会调用Saga参与者,但参与者不会调用协调器。
- 程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。
- 易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试。
事件/编排设计优点如下:
- 避免中央协调器单点故障风险。
- 当涉及的步骤较少服务开发简单,容易实现。
Saga事务缺点
命令协调设计缺点如下:
- 中央协调器容易处理逻辑容易过于复杂,导致难以维护。
- 存在协调器单点故障风险。
事件/编排设计缺点如下:
- 服务之间存在循环依赖的风险。
- 当涉及的步骤较多,服务间关系混乱,难以追踪调测。
由于Saga模型中没有Prepare阶段,因此事务间不能保证隔离性。
当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。
事务解决方案总结
总结一下各个方案的常见的使用场景:
2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。
本地消息表/MQ 事务:都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。
Saga 事务:由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。



