1、简介
1.1、背景
我们所做的项目中经常涉及到服务A调服务B、服务B调服务C这种情况,当整个调用链路中某一个服务发生异常时,需要将所涉及所有服务做的修改都回滚掉才能保证整体事务的一致性。Spring所提供的声明式事务@Transcational只能保证服务在操作自身数据库时的事务一致性,而无法保证其他服务的事务与自身保持一致。
1.2、我们真的需要分布式事务吗?
引入任何一个技术之前我觉得都应该想清楚真的需要吗?其实并不是所有的服务间调用都有一致性事务的问题。例如以下几种场景,个人认为并不需要分布式事务:
- 在一套大的单点登录系统中,只会有一个服务管理用户信息,我们可以设计为其他的服务可以获取用户信息,但是不能修改,那么这样的设计就天然的避免了分布式事务。
- zipkin这种调用链采集服务,通常我们在调用时并不需要过多的关注成功与否,也不会让它的调用结果影响正常业务流程。
再看可能需要分布式事务的场景,简单的服务A调用服务B伪代码:
@Transcational
public void method(){
//修改服务A数据库
doSomeThingForA1();
//调用服务B接口(服务B接口中有修改服务B数据库的操作)
accessServiceB();
//再次修改服务A数据库
doSomeThingForA2();
}
若是方法doSomeThingForA1中发生异常,对数据库A的操作将回滚,不会调用服务B接口;
若是方法accessServiceB发生异常,服务B中发生错误自身会回滚,服务A接收到错误也抛出异常回滚;
若是方法doSomeThingForA2发生异常,对数据库A的操作可以回滚,但是服务B的数据库将无法回滚。
这么一分析,我们是不是在允许的情况下改造一下代码顺序就可以呢?
@Transcational
public void method(){
//修改服务A数据库
doSomeThingForA();
//调用服务B接口(服务B接口中有修改服务B数据库的操作)
accessServiceB();
}
将对数据库A的修改操作全部写在调用服务B接口之前,这样一旦doSomeThingForA发生任何异常,就不会再调用B了,而B接口发生任何异常两边的操作也都可以回滚。
但是要强调的是”允许的情况下“,如果无法避免第一种情况的发生,我们还是要从分布式事务的角度去解决一致性问题。
这么一分析是不是感觉大部分项目都不需要这玩意了,毕竟不是每个系统都像天猫、淘宝那样的,哈哈哈。。。
1.3、分布式事务的特性
本地事务我们强调的是ACID,即原子性、一致性、隔离性、持久性。ACID对于事务的要求太过严格,对于高并发的分布式系统来说,需要作出一些取舍来保证性能。那么分布式事务关注什么呢?
- Consistency:一致性
- Availability:可用性
- Partition tolerance:分区容错性
其实个人认为CAP并不仅是分布式事务的特性,几乎所有的分布式相关组件都在考虑CAP。我们在聊到euraka、zookeeper等等这些东西的时候也会提到CAP。分布式组件很难同时满足三者,所以才出现了那么多CA、CP、AP的组件。
既然CAP很难同时满足,便出现了退而求其次的BASE理论:
- Basically Available:基本可用
- Soft State:软状态
- Eventual Consistency:最终一致性
2、常用解决方案
目前为止应该没有完美的解决方案,但是我们可以通过借鉴那些大型系统中用过的方案,了解其优缺点,根据实际项目分析,设计出自己的分布式事务方案。
首先,了解几个关键的概念:
- XA:由X/Open组织提出的分布式事务的规范。 XA规范主要定义了(全局)事务管理器™和(局 部)资源管理器(RM)之间的接口。主流的关系型 数据库产品都是实现了XA接口的。
- RM:资源管理器(Resource Manager),用来管理系统资源,是通向事务资源的途径。数据库就是一种资源管理器。资源管理还应该具有管理事务提交或回滚的能力。
- TM:事务管理器(Transaction Manager),事务管理器是分布式事务的核心管理者。事务管理器与每个资源管理器RM进行通信,协调并完成事务的处理。
参考
2.1、两阶段提交协议
2.1.1 协议原理
两阶段提交协议是协调所有分布式原子事务参与者,并决定提交或取消(回滚)的分布式算法。
在两阶段提交协议中,系统一般包含两类角色:协调者(coordinator),和事务参与者(participants,cohorts或workers)。协调者通常一个系统只有一个,而参与者一般包含多个。
2.1.2 两个阶段的执行
-
请求阶段(commit-request phase,或称表决阶段,voting phase)
在请求阶段,协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。
在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。 -
提交阶段(commit phase)
在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或取消。
当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。
参与者在接收到协调者发来的消息后将执行响应的操作。
2.1.3 方案缺点
-
同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。
当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。 -
单点故障。由于协调者的重要性,一旦协调者发生故障。
参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题) -
数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。
而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。 -
两阶段提交无法解决的问题
当协调者出错,同时参与者也出错时,两阶段无法保证事务执行的完整性。
考虑协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
2.2、 三阶段提交协议
2.2.1 协议简介
三阶段提交协议在协调者和参与者中都引入超时机制,并且把两阶段提交协议的第一个阶段拆分成了两步:询问,然后再锁资源,最后真正提交。
2.2.2 三个阶段的执行
-
CanCommit阶段
3PC的CanCommit阶段其实和2PC的准备阶段很像。
协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。 -
PreCommit阶段
Coordinator根据Cohort的反应情况来决定是否可以继续事务的PreCommit操作。
根据响应情况,有以下两种可能。
A.假如Coordinator从所有的Cohort获得的反馈都是Yes响应,那么就会进行事务的预执行:
发送预提交请求。Coordinator向Cohort发送PreCommit请求,并进入Prepared阶段。
事务预提交。Cohort接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
响应反馈。如果Cohort成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
B.假如有任何一个Cohort向Coordinator发送了No响应,或者等待超时之后,Coordinator都没有接到Cohort的响应,那么就中断事务:
发送中断请求。Coordinator向所有Cohort发送abort请求。
中断事务。Cohort收到来自Coordinator的abort请求之后(或超时之后,仍未收到Cohort的请求),执行事务的中断。 -
DoCommit阶段
该阶段进行真正的事务提交,也可以分为以下两种情况:
(1) 执行提交
A.发送提交请求。Coordinator接收到Cohort发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有Cohort发送doCommit请求。
B.事务提交。Cohort接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
C.响应反馈。事务提交完之后,向Coordinator发送ACK响应。
D.完成事务。Coordinator接收到所有Cohort的ACK响应之后,完成事务。
(2) 中断事务
Coordinator没有接收到Cohort发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
三阶段提交协议和两阶段提交协议的不同
对于协调者(Coordinator)和参与者(Cohort)都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到cohort的消息则默认失败)。
在2PC的准备阶段和提交阶段之间,插入预提交阶段,使3PC拥有CanCommit、PreCommit、DoCommit三个阶段。
PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。
2.2.3 协议缺点
如果进入PreCommit后,Coordinator发出的是abort请求,假设只有一个Cohort收到并进行了abort操作,
而其他对于系统状态未知的Cohort会根据3PC选择继续Commit,此时系统状态发生不一致性。
2.3 TCC
2.3.1 协议简介
TCC是服务化的两阶段编程模型,其Try、Confirm、Cancel 3个方法均由业务编码实现。其中Try操作作为一阶段,负责资源的检查和预留,Confirm操作作为二阶段提交操作,执行真正的业务,Cancel是预留资源的取消。
2.3.2 实现注意事项
(1) 业务操作分为两个阶段
接入TCC前,业务操作只需要一步就能完成,但是在接入TCC之后,需要考虑如何将其分成2阶段完成,把资源的检查和预留放在一阶段的Try操作中进行,把真正的业务操作的执行放在二阶段的Confirm操作中进行。
(2) 允许空回滚
如下图所示,事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因为丢包而导致的网络超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;
TCC服务在未收到Try请求的情况下收到Cancel请求,这种场景被称为空回滚;TCC服务在实现时应当允许空回滚的执行;
(3) 防悬挂控制
如下图所示,事务协调器在调用TCC服务的一阶段Try操作时,可能会出现因网络拥堵而导致的超时,此时事务协调器会触发二阶段回滚,调用TCC服务的Cancel操作;在此之后,拥堵在网络上的一阶段Try数据包被TCC服务收到,出现了二阶段Cancel请求比一阶段Try请求先执行的情况;
用户在实现TCC服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段Try请求
(4) 幂等控制
无论是网络数据包重传,还是异常事务的补偿执行,都会导致TCC服务的Try、Confirm或者Cancel操作被重复执行;用户在实现TCC服务时,需要考虑幂等控制,即Try、Confirm、Cancel 执行一次和执行多次的业务结果是一样的
(5) 业务数据可见性控制
TCC服务的一阶段Try操作会做资源的预留,在二阶段操作执行之前,如果其他事务需要读取被预留的资源数据,那么处于中间状态的业务数据该如何向用户展示,需要业务在实现时考虑清楚;通常的设计原则是“宁可不展示、少展示,也不多展示、错展示”
(6) 业务数据并发访问控制
TCC服务的一阶段Try操作预留资源之后,在二阶段操作执行之前,预留的资源都不会被释放;如果此时其他分布式事务修改这些业务资源,会出现分布式事务的并发问题;
用户在实现TCC服务时,需要考虑业务数据的并发控制,尽量将逻辑锁粒度降到最低,以最大限度的提高分布式事务的并发性。
2.3.3 协议优缺点
- 优点:不与具体的服务框架耦合,位于业务服务层,而不是资源层,可以灵活的选择业务资源的锁定粒度。TCC里对每个服务资源操作的是本地事务,数据被锁住的时间短,可扩展性好,可以说是为独立部署的SOA服务而设计的。
- 缺点:实现TCC操作的成本较高,业务活动结束的时候Confirm和Cancel操作的执行成本。业务活动的日志成本。
使用范围:强隔离性,严格一致性要求的业务活动。适用于执行时间较短的业务,比如处理账户或者收费等等。
2.4、 基于可靠消息的最终一致性方案
2.4.1 方案简介
(1) A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就直接取消操作别执行了;
(2) 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消息,如果失败就告诉 mq 回滚消息;
(3) 如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务;
(4) mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,而确认消息却发送失败了。
(5) 这个方案里,要是系统 B 的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A 也回滚;或者是发送报警由人工来手工回滚和补偿。
2.4.2 方案优缺点
- 优点: 实现了最终一致性,不需要依赖本地数据库事务。
- 缺点: 实现难度大,主流MQ不支持,RocketMQ事务消息部分代码也未开源。
3、开源框架
3.1 LCN
3.1.1 简介
官网:https://www.txlcn.org/zh-cn/
LCN框架在2017年6月份发布第一个版本,目前已经更新到5.0。LCN的名称来源于其三个核心步骤的首字母
- 锁定事务单元(lock)
- 确认事务模块状态(confirm)
- 通知事务(notify)
5.0以后由于框架兼容了LCN、TCC、TXC三种事务模式,为了避免区分LCN模式,特此将LCN分布式事务改名为TX-LCN分布式事务框架。
框架定位:
LCN并不生产事务,LCN只是本地事务的协调工
3.2 事务控制原理
TX-LCN由两大模块组成, TxClient、TxManager,TxClient作为模块的依赖框架,提供TX-LCN的标准支持,TxManager作为分布式事务的控制者。事务发起方或者参与方都由TxClient端来控制。
3.2 Seata
3.2.1 简介
官网:https://github.com/seata/seata/wiki/%E6%A6%82%E8%A7%88
Seata(曾用名Fescar,开源版本GTS)是阿里的开源分布式事务框架。设计上注重:
- 对业务无侵入: 这里的 侵入 是指,因为分布式事务这个技术问题的制约,要求应用在业务层面进行设计和改造。这种设计和改造往往会给应用带来很高的研发和维护成本。我们希望把分布式事务问题在 中间件 这个层次解决掉,不要求应用在业务层面做额外的工作。
- 高性能: 引入分布式事务的保障,必然会有额外的开销,引起性能的下降。我们希望把分布式事务引入的性能损耗降到非常低的水平,让应用不因为分布式事务的引入导致业务的可用性受影响。
3.2.2 原理
一个典型的分布式事务过程:
(1) TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
(2) XID 在微服务调用链路的上下文中传播。
(3) RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
(4) TM 向 TC 发起针对 XID 的全局提交或回滚决议。
(5) TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
还有很多的细节见官网了,官网图文并茂写得很清楚。。。
另还有开源框架Saga也值得了解一下。