分布式事务详解
一. 分布式事务的概念
随着分布式计算的发展,事务在分布式计算领域也得到了广泛的应用.在单机数据库中,我们很容易能够实现一套满足 ACID
特性的事务处理系统,但在分布式数据库中,数据分散在各台不同的机器上,如何对这些数据进行分布式的事务处理具有非常大的挑战.
分布式事务(Distributed Transaction)是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点上,通常一个分布式事务中会涉及对多个数据源或业务系统的操作.简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败.本质上来说,分布式事务就是为了保证不同数据库的数据一致性!
可以设想一个最典型的分布式事务场景:一个跨银行的转账操作涉及到调用两个异地的银行服务,其中一个是本地银行提供的存款服务,另一个则是目标银行提供的取款服务,这两个服务本身是无状态并且相互独立的,共同构成了一个完整的分布式事务.如果从本地银行存款成功,但是因为某种原因取款服务失败了,那么就必须回滚到取款之前的状态,否则用户可能会发现自己的钱不翼而飞了.
从这个例子可以看到,一个分布式事务可以看做是多个分布式的操作序列组成的,例如上面例子的取款服务和存款服务,通常可以把这一系列分布式的操作序列称为子事务.因此,分布式事务也可以被定义为一种嵌套型的事务,同时也就具有了 ACID
事务特性.但由于在分布式事务中,各个子事务的执行是分布式的,因此要实现一种能够保证 ACID
特性的分布式事务处理系统就显得格外复杂.
二. 分布式事务产生的背景原因
根据本地事务的特征来看,我们可以将分布式事务的产生分为两块,一个是由于Service(业务)产生了多个节点,另一个是Resource(数据库)产生了多个节点.
1. 应用SOA化,造成多个Service节点
所谓的SOA化,就是业务的服务化.比如原来单机支撑了整个电商网站的用户交易平台,现在业务夸大需要对用户交易平台进行拆解,分离出了余额中心、积分中心、优惠券中心等.对于余额中心,有专门的余额数据库存储余额信息,积分中心有专门的积分数据库存储用户的积分信息,优惠券中心也会有专门的优惠券数据库存储优惠券信息.这时候如果要进行充值操作,那么就会涉及到余额数据库,积分数据库和优惠券数据库,为了保证数据一致性,就需要用到分布式事务.

2. 数据库分库分表,造成多个Resource节点
当数据库单表一年产生的数据超过1000W,那么就要考虑分库分表,具体分库分表的原理在此不做解释,简单的说就是原来的一个数据库变成了多个数据库.这时候,如果一个操作既要访问Db01,又访问Db02,而且要保证数据的一致性,那么就要用到分布式事务.

3. 分布式事务应用场景
场景1--支付
分布式事务最经典的场景就是支付了.一笔支付,是对买家账户进行扣款,同时对卖家账户进行加钱,这些操作必须在一个事务里执行,要么全部成功,要么全部失败.而对于买家账户属于买家中心,对应的是买家数据库;而卖家账户属于卖家中心,对应的是卖家数据库,对不同数据库的操作必然需要引入分布式事务.
场景2--在线下单
买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性.
四. 分布式事务的理论基础
从上面来看分布式事务是随着互联网的高速发展应运而生的,我们之前说过数据库的ACID
四大特性,但是在分布式事务中已经无法满足了,这个时候又有了一些新的理论来作为分布式事务的理论基础:
1. CAP定理
2000 年,加州大学伯克利分校计算机教授Eric Brewer提出了著名的 CAP
理论,又被叫作布鲁尔定理,任何基于网络的数据共享系统(即分布式系统)最多只能满足数据一致性(Consistency)
、可用性(Availability)
和网络分区容错(Partition Tolerance)
三个特性中的两个.对于设计分布式系统来说(不仅仅是分布式事务)的架构师来说,CAP就是入门理论.
Consistency(一致性):对某个指定的客户端来说,任何一个读操作都能返回最新的写操作.对于分布在不同节点上的数据上来说,如果在某个节点上更新了数据,在其他节点上都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致性.
Availability(可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应).可用性的两个关键一个是合理的时间,一个是合理的响应.合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回;合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回50,而不是返回40或其他.
Partition Tolerance(分区容错性):当出现网络分区后,系统能够继续工作.打个比方,这里个集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作.
根据CAP理论我们知道,CAP三者是不能同时实现的.在分布式系统中,网络无法100%保证可靠,网络分区是一个必然现象.如果我们选择了CA而放弃了P,那么当发生分区现象时,为了保证一致性,这个时候必须拒绝请求,但是A又不允许,所以分布式系统理论上不可能选择CA架构,只能选择CP或者AP架构.
对于CP来说,放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致;
对于AP来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的BASE理论也是根据AP扩展来的.
在CAP理论中是忽略网络延迟的,也就是当事务提交时,从节点A实时同步到节点B,但在现实中这个是不可能的,所以总会有一段时间内是不一致的.在CAP中同时选择两个,比如你选择了CP,并不是叫你放弃A.因为P出现的概率实在是太小了,大部分的时间你仍然需要保证CA.就算网络分区出现了你也要为后来的A做准备,比如通过一些日志的手段,使其他机器恢复至可用.
2. BASE理论
BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个单词的缩写,是对CAP理论中对AP的一个发展.
Basically Available基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用;
Soft state软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致性;
Eventually consistent最终一致性:最终一致性是指经过一段时间后,所有节点数据都将会达到一致.
BASE理论解决了CAP理论中的网络延迟导致的问题,在BASE理论中用软状态和最终一致性,保证了网络延迟后的一致性.BASE理论和 ACID
是相反的,它完全不同于ACID
的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致性状态.
五. 分布式事务的解决方案
1. 概述
在介绍分布式事务的方案之前,首先我们一定要思考明确一下,我们是否真的需要用到分布式事务?
上面说过出现分布式事务的两个原因,其中有一个原因是因为微服务过多.可能有太多的团队里面一个人维护几个微服务,太多的团队进行过度设计,搞得所有人都疲劳不堪,而微服务过多就会引出分布式事务,这个时候我们不建议你去采用下面任何一种解决方案,而是考虑把需要用到分布式事务的微服务聚合成一个单机服务,使用数据库的本地事务即可.因为不论任何一种方案都会增加你系统的复杂度,这样的成本实在是太高了.千万不要因为追求某些设计,而引入不必要的成本和复杂度.
如果你确定需要引入分布式事务可以看看下面几种常见的方案.
2. 2PC 解决方案
2.1 XA协议介绍
说到2PC
就不得不聊数据库分布式事务中的 XA Transactions
协议.
XA Transactions
协议是X/Open CAE Specification (Distributed Transaction Processing)
模型中定义的TM(Transaction Manager)
与RM(Resource Manager)
之间进行通信的接口.在XA
规范中,数据库充当RM角色,应用程序需要充当TM的角色,TM用来即生成全局的txId
,调用XAResource
接口,把多个本地事务协调为全局统一的分布式事务.

2.2 XA协议中的两个阶段
两阶段示意图:

第一阶段--prepare投票阶段:事务管理器要求每个涉及到分布式事务的数据库 预提交(Precommit) 此操作,并反映是否可以提交;
第二阶段--commit/rollback提交阶段:事务协调器要求每个数据库提交数据,或者回滚数据.
2.3 2PC 概念
二阶段提交是对XA
协议的标准实现,它将分布式事务的提交拆分为2个阶段:prepare和commit/rollback.在分布式事务中最常用的解决方案就是二阶段提交.
在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败.当一个事务跨越多个节点时,为了保持事务的ACID
特性,需要引入一个作为协调者(通常一个系统中只有一个)的组件来统一掌控所有参与者(一般包含多个)节点的操作结果并最终指示这些节点是否要把操作结果进行真正的提交.
因此,二阶段提交的算法思路可以概括为:参与者将操作成败的结果通知协调者,再由协调者根据所有参与者的反馈情况决定各参与者是否要提交操作还是中止操作.
2.4 2PC 详解
第一阶段:Prepare投票阶段:
该阶段的主要目的在于打探数据库集群中的各个参与者是否能够正常的执行事务,具体步骤如下:
1️⃣. 协调者向所有的分布式事务参与者发送执行事务的请求,并等待事务参与者反馈事务执行结果;
2️⃣. 事务参与者收到执行请求之后,开始执行事务,但不提交事务,并记录事务日志;
3️⃣. 参与者将自己事务执行情况反馈给协调者,同时阻塞线程等待协调者的后续指令.
第二阶段:事务提交阶段
在经过第一阶段协调者的消息打探之后,各个参与者会回复各自事务的执行情况,这时候存在三种可能:
1️⃣. 所有的参与者回复能够正常执行事务;
2️⃣. 一个或多个参与者回复事务执行失败;
3️⃣. 协调者等待超时.
事务提交阶段的3种执行情况分析
对于第一种情况,分布式事务协调者将向所有的参与者发出提交事务的通知,具体步骤如下:
1️⃣. 协调者向各个参与者发送commit通知,请求提交事务;
2️⃣. 参与者收到事务提交通知之后,执行commit操作,然后释放占有的资源;
3️⃣. 参与者向协调者返回事务commit的结果信息.

对于第二、三种情况,协调者均认为参与者无法正常成功执行事务,为了整个集群数据的一致性,所以要向各个参与者发送事务回滚通知,具体步骤如下:
1️⃣. 协调者向各个参与者发送事务rollback通知,请求回滚事务;
2️⃣. 参与者收到事务回滚通知之后,执行rollback操作,然后释放占有的资源;
3️⃣. 参与者向协调者返回事务rollback的结果信息.

2.5 2PC 的优缺点
优点:
尽量保证了数据的强一致,实现成本较低,在各大主流数据库中都有自己的实现,MySQL是从5.5开始支持的.
缺点:
单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用.
同步阻塞:在准备就绪之后,需要等待所有参与子事务的反馈,所以资源管理器中的资源一直处于阻塞,直到提交完成,才会释放资源,因此可能造成数据库资源锁定时间过长.
数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性.
2.6 2PC 总结
总的来说,2PC比较简单,成本较低,但是其存在单点问题,以及不能支持高并发(由于同步阻塞),所以不适合并发高以及子事务生命周长较长的业务场景.两阶段提交这种解决方案属于牺牲了一部分可用性来换取的一致性.
3. 3PC 解决方案
3.1 3PC 概念
针对两阶段提交存在的问题,三阶段提交协议通过引入一个 “预询盘” 阶段,以及超时策略来减少整个集群的阻塞时间,提升系统性能.三阶段提交的三个阶段分别为:can_commit,pre_commit,do_commit
3.2 3PC 详解
第一阶段: can_commit
在该阶段协调者会去询问各个参与者是否能够正常执行事务,参与者根据自身情况回复一个预估值,相对于真正的事务执行,这个过程是轻量的,具体步骤如下:
1️⃣. 协调者向各个事务参与者发送事务询问通知,询问是否可以执行事务操作,并等待回复;
2️⃣. 各个事务参与者依据自身状况回复一个预估值,如果预估自己能够正常执行事务就返回确定信息,并进入预备状态,否则返回否定信息.
第二阶段: pre_commit
本阶段协调者会根据第一阶段的预询盘结果采取相应操作,询盘结果主要有三种:
1️⃣. 所有的参与者都返回确定信息;
2️⃣. 一个或多个参与者返回否定信息;
3️⃣. 协调者等待超时.
事务预提交阶段的3种执行情况分析
针对第一种情况,协调者会向所有参与者发送事务执行请求,具体步骤如下:
1️⃣. 协调者向所有的事务参与者发送事务执行通知;
2️⃣. 参与者收到通知后,执行事务,但不提交;
3️⃣. 参与者将事务执行情况返回给客户端.
在上面的步骤中,如果参与者等待超时,则会中断事务.针对第二、三种情况,协调者认为事务无法正常执行,于是向各个参与者发出abort
通知,请求退出预备状态,具体步骤如下:
1️⃣. 协调者向所有事务参与者发送abort通知;
2️⃣. 参与者收到通知后,中断事务.

第三阶段: do_commit
如果第二阶段事务未中断,那么本阶段协调者将会依据事务执行返回的结果来决定提交或回滚事务,分为三种情况:
1️⃣. 所有的参与者都能正常执行事务;
2️⃣. 一个或多个参与者执行事务失败;
3️⃣. 协调者等待超时.
针对第一种情况,协调者向各个参与者发起事务提交请求,具体步骤如下:
1️⃣. 协调者向所有参与者发送事务commit通知;
2️⃣. 所有参与者在收到通知之后执行commit操作,并释放占有的资源;
3️⃣. 参与者向协调者反馈事务提交结果.
针对第二、三种情况,协调者认为事务无法正常执行,于是向各个参与者发送事务回滚请求,具体步骤如下:
1️⃣. 协调者向所有参与者发送事务rollback通知;
2️⃣. 所有参与者在收到通知之后执行rollback操作,并释放占有的资源;
3️⃣. 参与者向协调者反馈事务提交结果.

在本阶段如果因为协调者或网络问题,导致参与者迟迟不能收到来自协调者的commit或rollback请求,那么参与者将不会如两阶段提交中那样陷入阻塞,而是等待超时后继续commit.相对于两阶段提交虽然降低了同步阻塞,但仍然无法避免数据的不一致性.
4. TCC(补偿事务) 解决方案--变种两阶段提交
4.1 TCC 概念
关于TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出.
所谓的TCC编程模式,其实是两阶段提交的一个变种.TCC提供了一个编程框架,将整个业务逻辑分为三块:Try、Confirm和Cancel
三个操作.以在线下单为例,Try阶段会去扣库存,Confirm阶段则是去更新订单状态,如果更新订单失败,则进入Cancel阶段,会去恢复库存.

4.2 TCC 实现思想
TCC
其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作接口.TCC模型是把锁的粒度完全交给业务处理.
4.3 TCC 详解
1️⃣.Try 阶段:尝试执行,完成所有业务检查(一致性),预留必须业务资源(准隔离性);
2️⃣.Confirm 阶段:主要是对业务系统做确认提交,确认执行真正执行业务,不作任何业务检查,只使用Try阶段预留的业务资源,Confirm操作要满足幂等性.Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm 阶段是不会出错的,如果Confirm失败后需要再次进行重试.即只要Try成功,Confirm一定成功;
3️⃣.Cancel 阶段:取消执行,释放Try阶段预留的业务资源, Cancel操作也要满足幂等性.Cancel阶段的异常和Confirm阶段异常处理方案基本上一致.
下面对TCC模式下,A账户往B账户汇款100元为例子,对业务的改造进行详细的分析.

汇款服务和收款服务都分别需要实现Try-Confirm-Cancel接口,并在业务初始化阶段将其注入到TCC事务管理器中.
[汇款服务]
Try:
检查A账户有效性,即查看A账户的状态是“转帐中”还是“冻结”;
检查A账户余额是否充足;
从A账户中扣减100元,并将状态置为“转账中”;
预留扣减资源,将从A往B账户转账100元这个事件存入消息或者日志中;
Confirm:
不做任何操作;
Cancel:
A账户增加100元;
从日志或者消息中,释放扣减资源.
[收款服务]
Try:
检查B账户账户是否有效;
Confirm:
读取日志或者消息,B账户增加100元;
从日志或者消息中,释放扣减资源;
Cancel:
不做任何操作.
由该案例可以看出,TCC模型对业务的侵入强,改造的难度大.
4.4 TCC 总结
TCC的适用场景
1️⃣.一些强隔离性,要求严格一致性的活动业务;
2️⃣.执行时间较短的业务.
TCC对比XA协议
TCC事务机制相比于上面介绍的XA协议,解决了XA协议的几个缺点:
1️⃣.解决了协调者单点故障,由主业务方发起并完成这个业务活动,业务活动管理器也变成多点,引入集群.
2️⃣.同步阻塞:引入超时,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小.
3️⃣.数据一致性:有了补偿机制之后,由业务活动管理器控制一致性.
TCC的缺点
TCC是通过代码人为的实现了两阶段提交,不同的业务场景所写的代码都不一样,复杂度也不一样,因此,这种模式对业务的侵入强,改造的难度大,并不能很好地被复用.
5. 本地消息表(异步确保)解决方案
5.1 本地消息表的概念
本地消息表这个方案最初是ebay
提出的,ebay
的完整方案请参考:https://queue.acm.org/detail.cfm?id=1394128
此方案的核心思想是将远程分布式事务拆分成一系列的本地事务,需要将分布式处理的任务通过消息日志的方式来异步执行,消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试.人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理.

5.2 本地消息表的基本思路
1️⃣. 消息生产方,需要额外创建一个消息表,并记录消息发送状态.消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面.然后消息会经过MQ发送到消息的消费方,如果消息发送失败,会进行重试发送.
2️⃣. 消息消费方,需要处理这个消息,并完成自己的业务逻辑.此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行.如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作.
3️⃣. 生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍.如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的.
5.3 本地消息表的案例分析详解
对于本地消息队列来说核心是把大事务转变为小事务.举个经典的跨行转账的例子来描述.
(1). 第一步,伪代码如下:扣款10000,通过本地事务保证了凭证消息插入到消息表中.

(2). 第二步,通知对方银行账户上加10000了,通常采用两种方式:
1️⃣.采用时效性高的MQ,由对方订阅消息并监听,有消息时自动触发事件;
2️⃣.采用定时轮询扫描的方式,去检查消息表的数据.
5.4 本地消息表 总结
本地消息表符合BASE理论,属于最终一致性模型,适用于对一致性要求不高的场景,实现这个模型时需要注意重试的幂等.
6. 事务消息(消息中间件)解决方案
6.1 事务消息的概念
事务消息作为一种异步确保型的事务,是将两个事务分支通过MQ进行异步解耦,事务消息的设计流程同样借鉴了两阶段提交理论,整体交互流程如下图所示:

6.2 事务消息的基本思想
事务消息依赖于支持“事务消息”的消息队列,其基本思想是利用消息中间件实施两阶段提交,将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功并且对外发消息成功,要么两者都失败.
6.3 事务消息的详细的流程

- 1️⃣.事务发起方首先发送prepare消息到MQ;
- 2️⃣.在发送prepare消息成功后执行本地事务;
- 3️⃣.根据本地事务执行结果返回commit或者是rollback;
- 4️⃣.如果消息是rollback,MQ将删除该prepare消息不进行下发,如果是commit消息,MQ将会把这个消息发送给consumer端;
- 5️⃣.如果执行本地事务过程中,执行端挂掉,或者超时,MQ将会不停的询问其同组的其它producer来获取状态;
- 6️⃣.Consumer端的消费成功机制有MQ保证.
6.4 消息事务的具体实现
【非事务性的消息中间件】
还是以跨行转账为例,我们很难保证在扣款完成之后对MQ投递消息的操作就一定能成功,在这种情况下一致性似乎很难保证.

- 1️⃣.操作数据库成功,向MQ中投递消息也成功,则不存在问题;
- 2️⃣.操作数据库失败,则不会向MQ中投递消息;
- 3️⃣.操作数据库成功,但是向MQ中投递消息时失败,向外抛出了异常,刚刚执行的更新数据库的操作也将被回滚.
从上面分析的几种情况来看,基本上能保证发送者发送消息的可靠性.我们再来分析下消费者端面临的问题:
- 1️⃣.消息出列后,消费者对应的业务操作要执行成功,如果业务执行失败,消息不能失效或者丢失,需要保证消息与业务操作一致;
- 2️⃣.尽量避免消息重复消费,即使重复消费,也不能因此影响业务结果.
【支持事务的消息中间件】
除了上面介绍的通过异常捕获和回滚的方式外,还有没有其他的实现事务消息的思路呢?
阿里巴巴的RocketMQ是支持一种事务消息的消息中间件,能够确保本地操作和发送消息达到本地事务一样的效果.
- 1️⃣. 第一阶段:RocketMQ在执行本地事务之前,会先发送一个Prepared消息,并且会持有这个消息的地址;
- 2️⃣. 第二阶段:执行本地事务操作;
- 3️⃣. 第三阶段:确认消息发送,通过第一阶段拿到的地址去访问消息,并修改状态,如果本地事务成功,则修改状态为已提交,否则修改状态为已回滚.

但是如果第三阶段的确认消息发送失败了怎么办?
RocketMQ会定期扫描消息集群中的事务消息,如果发现了prepare状态的消息,它会向消息发送者确认本地事务是否已执行成功,如果成功则判断是回滚还是继续发送确认消息,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息.这样就保证了消息发送与本地事务同时成功或同时失败.
6.5 消息事务的总结
相比本地消息表的方式,事务消息通过消息中间件来保证本地事务与消息的原子性,但实现了“事务消息”的消息队列比较少,还不够通用.目前主流的开源MQ(ActiveMQ、RabbitMQ、Kafka)
均未实现对事务消息的支持,比较遗憾的是,RocketMQ事务消息部分的代码也并未开源,需要自己去实现.
不管是本地消息表还是事务消息,都需要保证从事务执行且仅仅执行一次.如果失败,则需要重试,但也不可能无限次的重试.在从事务最终失败的情况下,是否需要通知主业务回滚?在此时,因为主事务已经提交,所以只能通过补偿来实现逻辑上的回滚.而当前时间点距主事务的提交已经有一定时间,回滚也可能失败.因此,最好是保证从事务逻辑上不会失败,万一失败,记录log并报警,人工介入.
7. 尽最大努力通知的解决方案
7.1 概念
最大努力通知方案主要也是借助MQ消息系统来进行事务控制,它是一种比较简单的分布式事务实现方案,本质上是通过定期校对,实现数据一致性.
7.2 最大努力通知方案的实现
- 业务活动的主动方,在完成业务处理之后,向业务活动的被动方发送消息,允许消息丢失;
- 主动方可以设置时间阶梯型通知规则,在通知失败后按规则重复通知,直到通知N次后不再通知;
- 主动方提供校对查询接口给被动方按需校对查询,用于恢复丢失的业务消息;
- 业务活动的被动方如果正常接收了数据,就正常返回响应,并结束事务;
- 如果被动方没有正常接收到数据,根据定时策略,向业务活动主动方查询,恢复丢失的业务消息.
7.3 最大努力通知方案的总结
- 1️⃣.服务模式:可查询操作、幂等操作;
- 2️⃣.被动方的处理结果不影响主动方的处理结果;
- 3️⃣.适用于对业务最终一致性的时间敏感度低的系统;
- 4️⃣.适合跨企业的系统间的操作,或者企业内部比较独立的系统间的操作,比如银行通知、商户通知等;
8. Saga事务 解决方案
8.1 Saga概念
Saga是30年前一篇数据库理论中提到的概念,最早是为了解决可能会长时间运行的分布式事务(long-running process)的问题.所谓long-running的分布式事务,是指那些企业的业务流程,需要跨应用、跨企业来完成某个事务,甚至在事务流程中还需要有手工操作参与的事务.这类事务的完成时间可能以分计,以小时计,甚至可能以天计.这类事务如果按照事务的ACID
的要求去设计,势必造成系统的可用性大大的降低.试想一个由两台服务器一起参与的事务,服务器A发起事务,服务器B参与事务,B的事务需要人工参与,所以处理时间可能很长.如果按照ACID
的原则,要保持事务的隔离性、一致性,服务器A中发起的事务中使用到的事务资源将会被锁定,不允许其他应用访问到事务过程中的中间结果,直到整个事务被提交或者回滚.这就造成事务A中的资源被长时间锁定,系统的可用性将不可接受.
而Saga,则是一种基于补偿的消息驱动的用于解决long-running process的解决方案,目标是为了在确保系统高可用的前提下尽量确保数据的一致性.还是上面的例子,如果用Saga来实现,那就是这样的流程:服务器A的事务先执行,如果执行顺利,那么事务A就先行提交;如果提交成功,那么就开始执行事务B,如果事务B也执行顺利,则事务B也提交,整个事务就算完成.但是如果事务B执行失败,那事务B本身需要回滚,这时因为事务A已经提交,所以需要执行一个补偿操作,将已经提交的事务A执行的操作作反操作,恢复到未执行前事务A的状态.这样的基于消息驱动的实现思路,就是Saga.我们可以看出,Saga是牺牲了数据的强一致性,仅仅实现了最终一致性,但是提高了系统整体的可用性.
8.2 Sage的实现思想
Sage的核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作.
8.3 Saga的组成
每个Saga由一系列sub-transaction Ti 组成,每个Ti 都有对应的补偿动作Ci,补偿动作用于撤销Ti造成的结果,这里的每个T都是一个本地事务.可以看到,和TCC相比,Saga没有“预留 try”动作,它的Ti就是直接提交到库.
8.4 Saga的恢复策略
T1, T2, T3, ..., Tn
T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n
Saga定义了两种恢复策略:
向后恢复:即上面提到的第二种执行顺序,其中j是发生错误的sub-transaction,这种做法的效果是撤销掉之前所有成功的sub-transation,使得整个Saga的执行结果撤销.
向前恢复:适用于必须要成功的场景,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn,其中j是发生错误的sub-transaction,该情况下不需要Ci.
这里要注意的是,在Saga模式中不能保证隔离性,因为没有锁住资源,其他事务依然可以覆盖或者影响当前事务.
还是拿100元买一瓶水的例子来说,这里定义:
T1=扣100元,T2=给用户加一瓶水,T3=减库存一瓶水
C1=加100元,C2=给用户减一瓶水,C3=给库存加一瓶水
我们一次进行T1,T2,T3这3个事务操作,如果发生问题,就对发生问题的事务执行反向对应的C操作.上面说到的隔离性的问题就会出现,如果执行到T3这个时候需要执行回滚,但是这个用户已经把水喝了(另外一个事务),回滚的时候就会发现无法给用户减一瓶水了,这就是事务之间没有隔离性的问题.
可以看见Saga模式没有隔离性的影响还是较大,可以参照华为的解决方案:从业务层面入手加入Session
以及锁的机制来保证能够串行化操作资源,也可以在业务层面通过预先冻结资金的方式隔离这部分资源.最后在业务操作的过程中可以通过及时读取当前状态的方式获取到最新的更新.
具体实例:可以参考华为的servicecomb
9. 分布式事务总结
9.1 不同分布式事务实现方案的对比

9.2 行业内对分布式事务的实现方案
Alipay的分布式事务服务DTS
分布式事务服务(Distributed Transaction Service,简称 DTS)是一个分布式事务框架,用来保障在大规模分布式环境下事务的最终一致性.DTS 从架构上分为 xts-client
和 xts-server
两部分,前者是一个嵌入客户端应用的 Jar
包,主要负责事务数据的写入和处理;后者是一个独立的系统,主要负责异常事务的恢复.
这应该属于我们上面所说的TCC模式.
eBay 本地消息表
本地消息表这种实现方式的思路,其实是源于ebay,后来通过支付宝等公司的布道,在业内广泛使用.其基本的设计思想是将远程分布式事务拆分成一系列的本地事务.如果不考虑性能及设计优雅,借助关系型数据库中的表即可实现.
类似使用本地消息表+消息通知的还有去哪儿,蘑菇街等
各种第三方支付回调
最大努力通知型:如支付宝、微信的支付回调接口,不断回调直至成功,或直至调用次数衰减至失败状态.
9.3 分布式事务实现方案的选择
经过上面这么多方案的介绍,那么在我们的项目中,应该采用哪种分布式事务的实现方案呢?
还是那句话,能不用分布式事务就不用,如果非得使用的话,结合自己的业务分析,看看自己的业务比较适合哪一种,是在乎强一致性,还是最终一致性.上面对解决方案只是做了一些简单介绍,如果真正的想要落地,其实每种方案需要思考的地方都非常多,复杂度都比较大,所以最后再次提醒一定要判断好是否使用分布式事务.
- 2PC/3PC需要资源管理器(mysql,redis)支持XA协议,且整个事务的执行期间需要锁住事务资源,会降低性能;
- TCC的模式,需要事务接口提供
try,confirm,cancel
三个接口,提高了编程的复杂性,需要依赖于业务方来配合提供这样的接口,推行难度大. - 最大努力通知型,适用于异构或者服务平台当中.
- 事务消息:ebay的分布式的事务是通过本地事务+可靠消息,来达到事务的最终一致性.但是事务消息这个解决方案可以把本地事务的工作给涵盖在事务消息当中.
9.4 分布式事务的思考问题
- 1️⃣.
ACID
和CAP
理论中的的 CA
是一样的吗? - 2️⃣.分布式事务常用的解决方案的优缺点是什么?分别适用于什么场景?
- 3️⃣.分布式事务出现的原因是什么?用来解决什么痛点?