概要
分布式事务是指会涉及到操作多个数据库的事务,目的是为了保证分布式系统中的数据一致性,分布式事务协议有:二阶段提交 2PC ,三阶段提交 3PC以及TCC协议。
两阶段提交协议(2PC)
两阶段提交协议,简称2PC(2 Prepare Commit),是比较常用的解决分布式事务问题的方式,要么所有参与进程都提交事务,要么都取消事务,即实现ACID中的原子性(A)的常用手段。
2PC执行流程
-
成功执行事务事务提交流程
阶段一:- 事务询问:协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
- 执行事务 (写本地的Undo/Redo日志)
- 各参与者向协调者反馈事务询问的响应
阶段二:
- 发送提交请求:协调者向所有参与者发出 commit 请求。
- 事务提交:参与者收到 commit 请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行期间占用的事务资源。
- 反馈事务提交的结果:参与者在完成事务提交之后,向协调者发送 Ack 信息。
- 完成事务:协调者接收到所有参与者反馈的 Ack 信息后,完成事务。
-
中断事务流程:
假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务
阶段一:- 事务询问:协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
- 执行事务 (写本地的Undo/Redo日志)
- 各参与者向协调者反馈事务询问的响应
阶段二:
- 发送回滚请求:协调者向所有参与者发出 Rollback 请求。
- 事务回滚:参与者接收到 Rollback 请求后,会利用其在阶段一中记录的 Undo 信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
- 反馈事务回滚结果:参与者在完成事务回滚之后,向协调者发送 Ack 信息。
- 中断事务:协调者接收到所有参与者反馈的 Ack 信息后,完成事务中断。
2PC优缺点
优点:原理简单
缺点:
- 同步阻塞:在二阶段提交的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态
- 数据不一致:在阶段二中,执行事务提交的时候,当协调者向所有的参与者发送Commit请求之后,发生了网络异常或者是协调者在尚未发送完Commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了Commit请求,于是会出现数据不一致的现象。
- 单点问题:若协调器出现问题,那么整个二阶段提交流程将无法运转,若协调者是在阶段二中出现问题时,那么其他参与者将会一直处于锁定事务资源的状态中,而无法继续完成事务操作。
三阶段提交协议(3PC)
出现背景:一致性协议中设计出了二阶段提交协议(2PC),但是2PC设计中还存在缺陷,于是就有了三阶段提交协议,这便是3PC的诞生背景。
3PC,全称 “three phase commit”,是 2PC 的改进版,将 2PC 的 “提交事务请求” 过程一分为二,共形成了由CanCommit、PreCommit和doCommit三个阶段组成的事务处理协议。
三阶段提交升级点(基于二阶段):
三阶段提交协议引入了超时机制。在第一阶段和第二阶段中,引入了一个准备阶段。保证了在最后提交阶段之前各参与节点的状态一致的。
简单讲:就是除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段
3个阶段详解
-
第一阶段(CanCommit 阶段):类似于2PC的准备(第一)阶段。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
- 事务询问:协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
- 响应反馈:参与者接到CanCommit请求之后,正常情况下, 如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。 否则 反馈No
-
第二阶段(PreCommit 阶段)
协调者根据参与者的反应情况来决定是否可以执行事务的PreCommit操作。根据响应情况,有以下两种情况:
- yes
发送预提交请求: 协调者向参与者发送PreCommit请求,并进入Prepared阶段。
事务预提交: 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中
响应反馈: 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
- no
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。则有:
发送中断请求: 协调者向所有参与者发送abort请求。
中断事务: 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断 -
第三阶段(doCommit 阶段)
该阶段进行真正的事务提交,也可以分为执行提交和中断事务两种情况。-
执行成功
(1).发送提交请求: 协调者接收到参与者发送的ACK响应,那么它将从预提交状态进入到提交状
态。 并向所有参与者发送doCommit请求。
(2).事务提交: 参与者接收到doCommit请求之后,执行正式的事务提交。 并在完成事务提交之
后释放所有事务资源。
(3).响应反馈: 事务提交完之后,向协调者发送ACK响应。
(4).完成事务: 协调者接收到所有参与者的ACK响应之后,完成事务。 -
中断事务
(1).发送中断请求: 协调者向所有参与者发送abort请求
(2).事务回滚: 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚
操作, 并在完成回滚之后释放所有的事务资源。
(3).反馈结果: 参与者完成事务回滚之后,向协调者发送ACK消息
(4).中断事务: 协调者接收到所有参与者反馈的ACK消息之后,执行事务的中断。
-
-
注意:一旦进入阶段三,可能会出现 2 种故障:
- 协调者出现问题
- 协调者和参与者之间的网络故障
如果出现了任一一种情况,最终都会导致参与者无法收到 doCommit 请求或者 abort 请求,针对这种情况,参与者都会在等待超时之后,继续进行事务提交
2PC对比3PC
- 首先对于协调者和参与者都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到参与者的消息则默认失败),主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
- 通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的 。
- PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的
3PC也会产生数据不一致问题,比如在第3阶段,协调者要发给参与的是回滚事务的命令,但是由于网络问题,协调者跟参与者通信超时了,这是参与者默认将事务提交了,这就会产生数据不一致的情况。
TCC
TCC是Try,Confirm,Cancel三个词语得缩写,TCC要求每个分支事务实现三个操作:预处理Try,确定Confirm,撤销Cancel。Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作即回滚操作。TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试。
锁定资源
从执行阶段来看,与传统事务机制中业务逻辑相同,但从业务角度来看,却不一样,TCC机制中的Try仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:
- 完成所有的业务检查
- 预留必须的业务资源
TCC事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
假设商品库存为100,购买数量为2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。
确认阶段
当Try阶段服务全部正常执行, 执行确认业务逻辑操作
这里使用的资源一定是Try阶段预留的业务资源。在TCC事务机制中认为,如果在Try阶段能正常的预留资源,那Confirm一定能完整正确的提交。Confirm阶段也可以看成是对Try阶段的一个补充,Try+Confirm一起组成了一个完整的业务逻辑。
取消
当Try阶段存在服务执行失败, 进入Cancel阶段
Cancel取消执行,释放Try阶段预留的业务资源,上面的例子中,Cancel操作会把冻结的库存释放,并更新订单状态为取消。
优缺点
优点
- 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
- 数据最终一致性:基于Confirm和Cancel的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
- 可靠性:解决了XA协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多
点,引入集群。
缺点
TCC的Try、Confirm和Cancel操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本
TCC常见异常
在分布式系统中,随时随地都需要面对网络超时,网络重发和服务器宕机等问题。所以分布式事务框架作为搭载在分布式系统之上的一个框架型应用也绕不开这些问题。
- 幂等处理
- 空回滚
- 资源悬挂
幂等处理
因为网络抖动等原因,分布式事务框架可能会重复调用同一个分布式事务中的一个分支事务的二阶段接口(Confirm或者Cancel)。所以分支事务的二阶段接口Confirm/Cancel需要能够保证幂等性,如果二阶段接口不能保证幂等性,则会产生严重的问题,造成资源的重复使用或者重复释放,进而导致业务故障。
应对策略
对于幂等类型的问题,通常的手段是引入幂等字段进行防重放攻击,对于分布式事务框架中的幂等问题,同样可以祭出这一利器。我们可以通过增加一张事务状态控制表来实现,这个表的关键字段有以下
几个:
- 主事务ID
- 分支事务ID
- 分支事务状态
其中1和2构成表的联合主键来唯一标识一笔分布式事务中的一条分支事务,3用来标识该分支事务的状态,一共有3种状态:
- INIT(I) - 初始化
- CONFIRMED© - 已提交
- ROLLBACKED® - 已回滚
幂等记录的插入时机是参与者的Try方法,此时的分支事务状态会被初始化为INIT,然后当二阶段的Confirm/Cancel执行时会将其状态置为CONFIRMED/ROLLBACKED。
当TC重复调用二阶段接口时,参与者会先获取事务状态控制表的对应记录查看其事务状态,如果状态已经为CONFIRMED/ROLLBACKED,那么表示参与者已经处理完其分内之事,不需要再次执行,可以直接返回幂等成功的结果给TC,帮助其推进分布式事务。
空回滚
当没有调用参与方Try方法的情况下,就调用了二阶段的Cancel方法(比如调try时超时了,会接着调Cancel),Cancel方法需要有办法识别出此时Try有没有执行,如果Try还没执行,表示这个Cancel操作是无效的,即本次Cancel属于空回滚;如果Try已经执行,那么执行的是正常的回滚逻辑。
应对策略
要应对空回滚的问题,就需要让参与者在二阶段的Cancel方法中有办法识别到一阶段的Try是否已经执行。
很显然,可以继续利用事务状态控制表来实现这个功能。
前面提到过为了保证幂等性,当Try方法被成功执行后,会插入一条记录,标识该分支事务处于INIT状态。所以后续当二阶段的Cancel方法被调用时,可以通过查询控制表的对应记录进行判断。如果记录存在且状态为INIT,就表示一阶段已成功执行,可以正常执行回滚操作,释放预留的资源;如果记录不存在则表示一阶段未执行,本次为空回滚,不释放任何资源。
资源悬挂
悬挂,顾名思义,是有一些资源被悬挂起来后续无法处理了,那么什么场景下才会出现这种现象呢?
但是考虑一种极端情况,当分布式事务到终态后,参与者的一阶段Try才被执行,此时参与者会根据业务需求预留相关资源,预留资源只有当前事务才能使用,然而此时分布式事务已经走到终态,后续再没有任何手段能够处理这些预留资源,至此,就形成了资源悬挂。
这种一阶段比二阶段执行的还晚的情况看似不可能,但是仔细考虑RPC调用的时序,其实这种情况在复
杂多变的网络中是完全可能的,下面的时序展示了这种可能性:
- 发起方通过RPC调用参与者一阶段Try,但是发生网络阻塞导致RPC超时
- RPC超时后,TC会回滚分布式事务(可能是发起方主动通知TC回滚或者是TC发现事务超时后回滚),调用已注册的各个参与方的二阶段Cancel
- 参与方空回滚后,发起方对参与者的一阶段Try才开始执行,进行资源预留从而形成悬挂
应对策略
资源悬挂的本质原因在于,一阶段和二阶段的执行顺序没有被严格地保证,所以相应的解决方案还是通过读取事务状态控制表的事务状态。
由于悬挂的产生背景是一阶段方法根本就未执行,所以此时事务控制记录是不存在的,需要在二阶段中处理ROLLBACK的情况(因为超时后触发回滚不可能存在二阶段为CONFIRM)。
处理方案为在判断为空回滚的场景下(体现在对应一阶段事务控制记录不存在),插入一条状态为ROLLBACKED的控制记录。
那么下次当一阶段Try抵达执行的时候,首先会尝试插入状态为INIT的事务控制记录。如果插入失败,表示当前分支事务的记录已经存在,Try无需继续执行。有几种可能性会导致此情形:
- 一阶段Try重复请求,网络抖动情况可能发生,可以理解为命中幂等
- 二阶段插入了防悬挂记录,一阶段不可继续执行