分布式事务
分布式事务
分布式事务产生的背景
在传统的单体项目中,多个不同的业务逻辑使用的都是同一个数据源,使用的都是同一个事务管理器,
所以不会存在事务问题。
在分布式或者微服务架构中,每个服务都有自己的数据源,使用不同事务管理器,
如果A服务去调用B服务,B服务执行失败了,A服务的事务和B服务的事务都会回滚,这时候是不存在事
务问题的,
但是如果A服务B服务执行成功之后出现异常,A服务的事务会回滚,但是B服务的事务不会回滚,此时
就存在分布式事务问题。
在单体的项目中,有多个不同的数据源,每个数据源中都有自己独立的事务管理器,互不影响,
那么这时候也会存在多数据源事务管理:需要通过分布式事务解决,比如seata 框架、lcn 框架
CAP
CAP是Consistency、Availablity和Partition-tolerance的缩写。
1、一致性(C:Consistency)
一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后
能够从一致性状态转移到另一个一致性状态。
对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致
性。
2、可用性(A:Availability)
可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值
来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。
在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有
限的时间内返回结果。
3、分区容错性(P:Partition Tolerance)
网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。
在分区容错性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用
性的服务,除非是整个网络环境都发生了故障。
其实在分布式系统中,分区容错性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实
际上是要在可用性和一致性之间做权衡。
可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时
为了保证一致性(CP),不能访问未同步完成的节点,也就失去了部分可用性;
为了保证可用性(AP),允许读取所有节点的数据,但是数据可能不一致。
为什么分布式系统无法同时保证一致性和可用性?
答:首先一个前提,对于分布式系统而言,分区容错性是一个最基本的要求,因此基本上我们在
设计分布式系统的时候只能从一致性(C)和可用性(A)之间进行取舍。
如果保证了一致性(C):对于节点N1和N2,当往N1里写数据时,N2上的操作必须被暂停,只
有当N1同步数据到N2时才能对N2进行读写请求,在N2被暂停操作期间客户端提交的请求会收到
失败或超时。显然,这与可用性是相悖的。
如果保证了可用性(A):那就不能暂停N2的读写操作,但同时N1在写数据的话,这就违背了一
致性的要求。
BASE
BASE 是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually
Consistent)三个短语的缩写。
1、基本可用(BA:Basically Available)
指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。
例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。
2、软状态(S:Soft State)
指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点
的数据副本之间进行同步的过程存在时延。
3、最终一致性(E:Eventually Consistent)
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态。
BASE 理论是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结,
是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。
BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的
方式来使系统达到最终一致性。
BASE 与ACID 的区别:
ACID 要求强一致性,通常运用在传统的数据库系统上。
而 BASE 要求最终一致性,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。
MySQL本地事务实现方案
MySQL数据库的事务实现原理
以MySQL 的InnoDB (InnoDB 是 MySQL 的一个存储引擎)为例,介绍一下单一数据库的事务实现原
理。
InnoDB 是通过 日志和锁 来保证的事务的 ACID特性,具体如下:
(1)通过数据库锁的机制,保障事务的隔离性;
(2)通过 Redo Log(重做日志)来,保障事务的持久性;
(3)通过 Undo Log (撤销日志)来,保障事务的原子性;
(4)通过 Undo Log (撤销日志)来,保障事务的一致性;
Undo Log 如何保障事务的原子性呢?
具体的方式为:在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为
Undo Log),然后进行数据的修改。如果出现了错误或者用户执行了 Rollback 语句,系统可以利用
Undo Log 中的备份将数据恢复到事务开始之前的状态。
Redo Log如何保障事务的持久性呢?
具体的方式为:Redo Log 记录的是新数据的备份(和 Undo Log 相反)。在事务提交前,只要将 Redo
Log 持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是 Redo Log 已经持
久化。系统可以根据 Redo Log 的内容,将所有数据恢复到崩溃之前的状态。
分布式事务分类:柔性事务和刚性事务
分布式场景下,多个服务同时对服务一个流程,比如电商下单场景,需要支付服务进行支付、库存服务
扣减库存、订单服务进行订单生成、物流服务更新物流信息等。如果某一个服务执行失败,或者网络不
通引起的请求丢失,那么整个系统可能出现数据不一致的原因。
上述场景就是分布式一致性问题,追根到底,分布式一致性的根本原因在于数据的分布式操作,引起的
本地事务无法保障数据的原子性引起。
分布式一致性问题的解决思路有两种,一种是分布式事务,一种是尽量通过业务流程避免分布式事务。
分布式事务是直接解决问题,而业务规避其实通过解决出问题的地方(解决提问题的人)。其实在真实业
务场景中,如果业务规避不是很麻烦的前提,最优雅的解决方案就是业务规避。
分布式事务分类
分布式事务实现方案从类型上去分刚性事务、柔型事务:
刚性事务满足CAP的CP理论
柔性事务满足BASE理论(基本可用,最终一致)
刚性事务
刚性事务:通常无业务改造,强一致性,原生支持回滚/隔离性,低并发,适合短事务。
原则:刚性事务满足足CAP的CP理论
刚性事务指的是,要使分布式事务,达到像本地式事务一样,具备数据强一致性,从CAP来看,
就是说,要达到CP状态。
刚性事务:XA 协议(2PC、JTA、JTS)、3PC,但由于同步阻塞,处理效率低,不适合大型网站分布式
场景。
柔性事务
柔性事务指的是,不要求强一致性,而是要求最终一致性,允许有中间状态,也就是Base理论,换句话
说,就是AP状态。
与刚性事务相比,柔性事务的特点为:有业务改造,最终一致性,实现补偿接口,实现资源锁定
接口,高并发,适合长事务。
柔性事务分为:
-
补偿型
-
异步确保型
-
最大努力通知型。
柔型事务:TCC/FMT、Saga(状态机模式、Aop模式)、本地事务消息、消息事务(半消息)
XA
AP: Application,应用程序。也就是业务层。哪些操作属于一个事务,就是AP定义的。
TM: Transaction Manager,事务管理器。接收AP的事务请求,对全局事务进行管理,管理事务分
支状态,协调RM的处理,通知RM哪些操作属于哪些全局事务以及事务分支等等。这个也是整个事务调
度模型的核心部分。
RM:Resource Manager,资源管理器。一般是数据库,也可以是其他的资源管理器,如消息队列
(如JMS数据源),文件系统等。
XA把参与事务的角色分成AP,RM,TM。
AP,即应用,也就是我们的业务服务。
RM指的是资源管理器,即DB,MQ等。
TM则是事务管理器。
AP自己操作TM,当需要事务时,AP向TM请求发起事务,TM负责整个事务的提交,回滚等。
2PC
二阶段提交协议将节点分为:
协调者角色(事务管理器Coordinator):负责向参与者发送指令,收集参与者反馈,做出提交或者回滚决
策
参与者角色(资源管理器Participant):接收协调者的指令执行事务操作,向协调者反馈操作结果,并
继续执行协调者发送的最终指令
阶段一:提交事务请求
- 事务询问。协调者向所有参与者发送事务内容,询问是否可以执行提交操作,并开始等待各参与者
进行响应; - 执行事务。各参与者节点,执行事务操作,并将Undo和Redo操作计入本机事务日志;
- 各参与者向协调者反馈事务问询的响应。成功执行返回Yes,否则返回No。
阶段二:执行事务提交,或者执行中断事务;
这一阶段包含两种情形:
执行事务提交,
执行中断事务
协调者在阶段二决定是否最终执行事务提交操作。
所有参与者reply Yes,那么执行事务提交。
当存在某一参与者向协调者发送No响应,或者等待超时。协调者只要无法收到所有参与者的Yes响
应,就会中断事务。
执行事务提交
所有参与者reply Yes,那么执行事务提交。
- 发送提交请求。协调者向所有参与者发送Commit请求;
- 事务提交。参与者收到Commit请求后,会正式执行事务提交操作,并在完成提交操作之后,释放
在整个事务执行期间占用的资源; - 反馈事务提交结果。参与者在完成事务提交后,写协调者发送Ack消息确认;
- 完成事务。协调者在收到所有参与者的Ack后,完成事务。
执行事务中断
事情总会出现意外,当存在某一参与者向协调者发送No响应,或者等待超时。协调者只要无法收到所
有参与者的Yes响应,就会中断事务。
- 发送回滚请求。协调者向所有参与者发送Rollback请求;
- 回滚。参与者收到请求后,利用本机Undo信息,执行Rollback操作。并在回滚结束后释放该事务
所占用的系统资源; - 反馈回滚结果。参与者在完成回滚操作后,向协调者发送Ack消息;
- 中断事务。协调者收到所有参与者的回滚Ack消息后,完成事务中断。
2PC具有明显的优缺点:
1、优点:
主要体现在实现原理简单;
2、缺点比较多:
- 同步阻塞导致性能问题
执行过程中,所有参与节点都是事务阻塞型的。
所有participant 都处于阻塞状态,各个participant 都在等待其他参与者响应,无法进行其他操作。
所有分支的资源锁定时间,由最长的分支事务决定。
另外当参与者锁定公共资源时,处于事务之外的其他第三方访问者,也不得不处于阻塞状态。
- 单点故障导致高可用(HA)问题:
协调者是个单点,一旦出现问题,各个participant 将无法释放事务资源,也无法完成事务操作;
并且,由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞下去。
尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完
成事务操作。
假设,协调者挂掉,可以重新选举一个协调者,但是,还是无法解决因为协调者宕机导致的参与者处于
阻塞状态的问题。
- 丢失消息导致的数据不一致问题:
如果协调者向所有参与者发送Commit请求后,发生局部网络异常,
或者协调者在尚未给全部的participant发送完Commit请求即出现崩溃,最终导致只有部分participant
收到、执行请求。
于是整个系统将会出现数据不一致的情形,why?
只有一部分参与者接受到了commit请求。
部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无
法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
- 过于保守:
二阶段提交协议没有设计较为完善的容错机制,任意一个节点的失败都会导致整个事务的失败。
具体来说:
2PC没有完善的容错机制,当参与者出现故障时,协调者无法快速得知这一失败,只能严格依赖超时设
置来决定是否进一步的执行提交还是中断事务。
总结一下: XA-两阶段提交协议(以2PC为参考)中会遇到的一些问题
1、性能问题
从流程上我们可以看得出,其最大缺点就在于它的执行过程中间,节点都处于阻塞状态,各个操作数据
库的节点此时都占用着数据库资源,
只有当所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资
源。
这样的过程会比较漫长,对性能影响比较大。
2、协调者单点故障问题
事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,会导致参与者收不到提交或回滚的通知,
从而导致参与者节点始终处于事务无法完成的中间状态。
3、丢失消息导致的数据不一致问题
在第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收
到提交消息,那么就会导致节点间数据的不一致问题。
4、过于保守:
二阶段提交协议没有设计较为完善的容错机制,任意一个节点的失败都会导致整个事务的失败。
3PC
所谓的三个阶段分别是:询问,然后再锁资源,最后真正提交。
第一阶段:CanCommit
第二阶段:PreCommit
第三阶段:Do Commit
阶段一:CanCommit
- 事务询问。协调者向所有参与者发送包含事务内容的canCommit的请求,询问是否可以执行事务
提交,并等待应答; - 各参与者反馈事务询问。正常情况下,如果参与者认为可以顺利执行事务,则返回Yes,否则返回
No。
阶段二:PreCommit
在本阶段,协调者会根据上一阶段的反馈情况来决定是否可以执行事务的PreCommit操作。有以下两种
可能:
执行事务预提交
中断事务
执行事务预提交
- 发送预提交请求。协调者向所有节点发出PreCommit请求,并进入prepared阶段;
- 事务预提交。参与者收到PreCommit请求后,会开始事务操作,并将Undo和Redo日志写入本机
事务日志; - 各参与者成功执行事务操作,同时将反馈以Ack响应形式发送给协调者,同事等待最终的Commit
或Abort指令。
中断事务
如果任意一个参与者向协调者发送No响应,或者等待超时,协调者在没有得到所有参与者响应时,即
可以中断事务。
中断事务的操作为:
- 发送中断请求。 协调者向所有参与者发送Abort请求;
- 中断事务。无论是participant 收到协调者的Abort请求,还是participant 等待协调者请求过程中
出现超时,参与者都会中断事务;
阶段三:doCommit
在这个阶段,会真正的进行事务提交,同样存在两种可能。
执行提交
回滚事务
执行提交
- coordinator发送提交请求。假如coordinator协调者收到了所有参与者的Ack响应,那么将从预提
交转换到提交状态,并向所有参与者,发送doCommit请求; - 事务提交。参与者收到doCommit请求后,会正式执行事务提交操作,并在完成提交操作后释放占
用资源; - 反馈事务提交结果。参与者将在完成事务提交后,向协调者发送Ack消息;
- 完成事务。协调者接收到所有参与者的Ack消息后,完成事务。
回滚事务
在该阶段,假设正常状态的协调者接收到任一个参与者发送的No响应,或在超时时间内,仍旧没收到
反馈消息,就会回滚事务:
- 发送中断请求。协调者向所有的参与者发送rollback请求;
- 事务回滚。参与者收到rollback请求后,会利用阶段二中的Undo消息执行事务回滚,并在完成回
滚后释放占用资源; - 反馈事务回滚结果。参与者在完成回滚后向协调者发送Ack消息;
- 回滚事务。协调者接收到所有参与者反馈的Ack消息后,完成事务回滚。
2PC和3PC的区别:
三阶段提交协议在协调者和参与者中都引入 超时机制,并且把两阶段提交协议的第一个阶段拆分成了
两步:询问,然后再锁资源,最后真正提交。
三阶段提交的三个阶段分别为:can_commit,pre_commit,do_commit。
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rollback请求时,会在等
待超时之后,继续进行事务的提交。
其实这个应该是基于概率来决定的,
当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,什么场景会产生PreCommit
请求呢?
协调者产生PreCommit请求的前提条件比较严格:是在第二阶段开始之前,收到所有参与者的
CanCommit响应都是Yes。
一句话概括就是:
当进入第三阶段时,由于网络超时/网络分区等原因,虽然参与者没有收到commit或者abort响应,
但是他有理由相信:成功提交的几率很大。
3PC主要解决的单点故障问题:
相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,
因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资
源并处于阻塞状态。
由于在docommit阶段,participant参与者如果超时,能自己决定提交本地事务,所以,3pc没有2pc那
么保守或者说悲观,或者说3pc更加的乐观。
3PC主要没有解决的数据一致性问题:
但是这种机制,还是有数据一致性问题,或者说,没有彻底解决数据一致性问题。
‘因为,由于网络原因,协调者发送的rollback命令没有及时被参与者接收到,那么参与者在等待超时之
后执行了commit操作。
这样就和其他接到rollback命令并执行回滚的参与者之间存在数据不一致的情况。
MQ事务消息方案
- 事务发起方首先发送半消息到MQ;
- MQ通知发送方消息发送成功;
- 在发送半消息成功后执行本地事务;
- 根据本地事务执行结果返回commit或者是rollback;
- 如果消息是rollback, MQ将丢弃该消息不投递;如果是commit,MQ将会消息发送给消息订阅
方; - 订阅方根据消息执行本地事务;
- 订阅方执行本地事务成功后再从MQ中将该消息标记为已消费;
- 如果执行本地事务过程中,执行端挂掉,或者超时,MQ服务器端将不停的询问producer来获取事
务状态; - Consumer端的消费成功机制有MQ保证;
RocketMQ实现MQ异步事务
以阿里的 RocketMQ 中间件为例,其思路大致为:
1.producer(本例中指A系统)发送半消息到broker,这个半消息不是说消息内容不完整, 它包含完整的
消息内容, 在producer端和普通消息的发送逻辑一致
2.broker存储半消息,半消息存储逻辑与普通消息一致,只是属性有所不同,topic是固定的
RMQ_SYS_TRANS_HALF_TOPIC,queueId也是固定为0,这个tiopic中的消息对消费者是不可见的,所
以里面的消息永远不会被消费。这就保证了在半消息提交成功之前,消费者是消费不到这个半消息的
3.broker端半消息存储成功并返回后,A系统执行本地事务,并根据本地事务的执行结果来决定半消息
的提交状态为提交或者回滚
4.A系统发送结束半消息的请求,并带上提交状态(提交 or 回滚)
5.broker端收到请求后,首先从RMQ_SYS_TRANS_HALF_TOPIC的queue中查出该消息,设置为完成状
态。如果消息状态为提交,则把半消息从RMQ_SYS_TRANS_HALF_TOPIC队列中复制到这个消息原始
topic的queue中去(之后这条消息就能被正常消费了);如果消息状态为回滚,则什么也不做。
6.producer发送的半消息结束请求是 oneway 的,也就是发送后就不管了,只靠这个是无法保证半消
息一定被提交的,rocketMq提供了一个兜底方案,这个方案叫消息反查机制,Broker启动时,会启动
一个TransactionalMessageCheckService 任务,该任务会定时从半消息队列中读出所有超时未完成的
半消息,针对每条未完成的消息,Broker会给对应的Producer发送一个消息反查请求,根据反查结果
来决定这个半消息是需要提交还是回滚,或者后面继续来反查
7.consumer(本例中指B系统)消费消息,执行本地数据变更(至于B是否能消费成功,消费失败是否重
试,这属于正常消息消费需要考虑的问题)
本地消息表方案
有时候我们目前的MQ组件并不支持事务消息,或者我们想尽量少的侵入业务方。这时我们需要另外一
种方案“基于DB本地消息表“。
本地消息表最初由eBay 提出来解决分布式事务的问题。是目前业界使用的比较多的方案之一,它的核
心思想就是将分布式事务拆分成本地事务进行处理。
本地消息表流程
发送消息方:
- 需要有一个消息表,记录着消息状态相关信息。
- 业务数据和消息表在同一个数据库,要保证它俩在同一个本地事务。直接利用本地事务,将业务数
- 据和事务消息直接写入数据库。
- 在本地事务中处理完业务数据和写消息表操作后,通过写消息到 MQ 消息队列。使用专门的投递
- 工作线程进行事务消息投递到MQ,根据投递ACK去删除事务消息表记录
- 消息会发到消息消费方,如果发送失败,即进行重试。
消息消费方:
- 处理消息队列中的消息,完成自己的业务逻辑。
- 如果本地事务处理成功,则表明已经处理成功了。
- 如果本地事务处理失败,那么就会重试执行。
- 如果是业务层面的失败,给消息生产方发送一个业务补偿消息,通知进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱
的自动对账补账逻辑,这种方案还是非常实用的。
本地消息表优缺点:
优点:
- 本地消息表建设成本比较低,实现了可靠消息的传递确保了分布式事务的最终一致性。
- 无需提供回查方法,进一步减少的业务的侵入。
- 在某些场景下,还可以进一步利用注解等形式进行解耦,有可能实现无业务代码侵入式的实现。
缺点:
- 本地消息表与业务耦合在一起,难于做成通用性,不可独立伸缩。
- 本地消息表是基于数据库来做的,而数据库是要读写磁盘IO的,因此在高并发下是有性能瓶颈的
TCC
TCC 提出了一种新的事务模型,基于业务层面的事务定义,锁粒度完全由业务自己控制,目的是
解决复杂业务中,跨表跨库等大颗粒度资源锁定的问题。
TCC 把事务运行过程分成 Try、Confirm / Cancel 两个阶段,每个阶段的逻辑由业务代码控制,
避免了长事务,可以获取更高的性能。
TCC的工作流程
TCC(Try-Confirm-Cancel)分布式事务模型相对于 XA 等传统模型,其特征在于它不依赖资源管理器(RM)
对分布式事务的支持,而是通过对业务逻辑的分解来实现分布式事务。
TCC 模型认为对于业务系统中一个特定的业务逻辑,其对外提供服务时,必须接受一些不确定性,即
对业务逻辑初步操作的调用仅是一个临时性操作,调用它的主业务服务保留了后续的取消权。如果主业
务服务认为全局事务应该回滚,它会要求取消之前的临时性操作,这就对应从业务服务的取消操作。而
当主业务服务认为全局事务应该提交时,它会放弃之前临时性操作的取消权,这对应从业务服务的确认
操作。每一个初步操作,最终都会被确认或取消。
因此,针对一个具体的业务服务,TCC 分布式事务模型需要业务系统提供三段业务逻辑:
- 初步操作 Try:完成所有业务检查,预留必须的业务资源。
- 确认操作 Confirm:真正执行的业务逻辑,不作任何业务检查,只使用 Try 阶段预留的业务资源。因此,只要 Try 操作成功,Confirm 必须能成功。另外,Confirm 操作需满足幂等性,保证一笔分布式事务有且只能成功一次。
- 取消操作 Cancel:释放 Try 阶段预留的业务资源。同样的,Cancel 操作也需要满足幂等性。
Try 阶段失败可以 Cancel,如果 Confirm 和 Cancel 阶段失败了怎么办?
TCC 中会添加事务日志,如果 Confirm 或者 Cancel 阶段出错,则会进行重试,所以这两个阶段需要支
持幂等;如果重试失败,则需要人工介入进行恢复和处理等。
TCC和2PC不同的是:
- XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。基于数据库锁实现,需要数据库支持XA协议,由于在执行事务的全程都需要对相关数据加锁,一般高并发性能会比较差
- TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁,性能较好。但是对微服务的侵入性强,微服务的每个事务都必须实现try、confirm、cancel等3个方法,开发成本高,今后维护改造的成本也高为了达到事务的一致性要求,try、confirm、cancel接口必须实现幂等性操作由于事务管理器要记录事务日志,必定会损耗一定的性能,并使得整个TCC事务时间拉长

浙公网安备 33010602011771号