来源:后端开发技术
继续分布式事务专题:本文讲解的是强一致性解决方案XA、2PC、3PC。
接着上一篇文章,由于我们非常关注数据的一致性,所以总体来说按照一致性强弱的维度分类,解决分布式事务问题可以有以下方案:
强一致性方案:XA协议,2PC(两阶段提交)、3PC(三阶段提交) 最终一致性方案:TCC,本地事务状态表、本地消息表、可靠消息最终一致性、RocketMQ 事务消息方案、最大努力通知方案,SAGA 弱一致性方案:基于业务补偿,定时任务对账
本文我们讲解强一致性方案:XA协议,2PC(两阶段提交)、3PC(三阶段提交)。如果上一篇基础内容还没有读,请移步。
CAP、BASE理论真的很重要!|分布式事务系列(一)
XA 协议
因为在使用本地事务的过程中有数据裤引擎的保证,但是如果是多数据源场景下就有了一致性问题,为此产生了全局事务。全局事务并不限定数据源是一个还是多个,但是在分布式系统场景下我们都当作多数据源的分布式事务来讨论。
为了解决分布式事务的一致性问题、统一标准,1991 年X/Open组织(后来并入国际开放标准组织The Open Group )提出了一套名为X/Open XA(XA 是 eXtended Architecture 的缩写)的处理事务架构,制定了标准化的模型和接口。它定义了全局的事务管理器(Transaction Manager,用于协调全局事务)和局部的资源管理器(Resource Manager,用于驱动本地事务)之间的通信接口。1994年国际开放标准组织The Open Group 在1994年定义了分布式事务处理模型 DTP(Distributed Transaction Processing Reference Model,X/Open XA),XA 协议成为事务模型事实上的标准。
在 XA 协议下有三种核心角色:
AP(Application Program):应用程序,指事物的发起者。 RM(Resource Managers):资源管理器,是分布式事务的参与者,管理共享资源,并提供访问接口,供外部程序来访问共享资源,比如数据库、打印服务等,另外 RM 还应该具有事务提交或回滚的能力。 TM(Transaction Manager):事务管理器,是分布式事务的协调者,管理全局事务,与每个RM进行通信,协调事务的提交和回滚,并协助进行故障恢复,此角色也可以由发起者担任。
基于 XA 规范 Java 中实现了 JTA ,MySQL、Oracle 也都对其做了支持,XA约定了TM和RM之间双向通讯的接口规范,能在一个TM和多个RM之间形成通信桥梁,通过协调多个数据源的一致动作,实现全局事务的统一提交或者统一回滚。
在XA协议下如果我们想实现一个商城的下单、扣款、扣库存功能,可以实现如下伪代码。
orderTransaction.begin();//订单
balanceTransaction.begin();//余额
warehouseTransaction.begin();//仓库
try{
order.submit();//下单
balance.pay();//付款
warehouse.decrease();//扣减库存
orderTransaction.commit();//订单
balanceTransaction.commit();//余额
//故障点
warehouseTransaction.commit();//仓库
}catch(Exception e){
//无法回滚
orderTransaction.rollback();//订单
balanceTransaction.rollback();//余额
warehouseTransaction.rollback();//仓库
}
订单、余额、仓库三个数据源分别开启事务,如果成功分别提交事务。但是这段逻辑有个很大的问题就是如果订单、余额事务提交成功,但是仓库commit发生异常,为了保证一致性需要全部回归,但是由于订单、余额已提交所以无法回滚,发生了数据不一致场景。面对这种问题,XA协议选择了两阶段提交(2PC)作为实现,从而在多个数据库资源下保证 ACID 四个特性。
准备阶段(投票阶段):协调者询问事务的所有参与者是否准备好提交,参与者如果已经准备好提交则回复 Prepared,否则回复 Non-Prepared。对于MySQL来说,准备阶段是在 Redo Log 中记录全部事务提交操作所要做的内容,并且锁定需要变更的资源,它与本地事务中真正提交的区别只是没有执行 commit 命令。这意味着在做完数据持久化后并不立即释放隔离性,即仍继续持有锁,维持数据对其他非事务内观察者的隔离状态。 提交阶段(执行阶段):协调者如果在上一阶段收到所有事务参与者回复的 Prepared 消息,则先自己在本地持久化事务状态为 Commit,在此操作完成后向所有参与者发送 Commit 指令,所有参与者立即执行提交操作;如果任意一个参与者回复了 Non-Prepared 消息,或任意一个参与者超时未回复,协调者将自己的事务状态持久化为 Abort 之后,向所有参与者发送 Abort 指令,参与者立即执行回滚操作。对于数据库来说,这个阶段的提交操作应是很轻量的,仅仅是持久化一条 Commit 命令,通常能够快速完成,只有收到 Abort 指令时,才需要根据 Undo Log 清理已提交的数据,这可能是相对重负载的操作。
关于两阶段的内容我们在这里不做过多解释,具体请看后面两阶段专题。
优缺点
由于 XA 使用两阶段提交,因此两阶段提交的优点和缺点通常适用于 XA。
优点:XA 允许跨多种异构技术的原子事务(不同类型数据源、消息中间件),解决了分布式事务的问题,而传统数据库事务仅限于单一数据库。
缺点:准备阶段会长时间持有资源,造成系统性能降低,并且协调者会有单点故障的问题。
总结
如果你看到这感觉晕晕乎乎,还是没明白XA到底是个啥,那需要看一下这段总结。
XA 协议是一种用于处理分布式事务的协议,定义了TM、RM、应用程序集中模型和接口,大多数实现XA的都是一些关系型数据库(包括MySQL,SQL Server、PostgreSQL 和Oracle)和消息中间件(包括ActiveMQ,HornetQ,MSMQ和IBM MQ),Java 中的 JTA 也实现了 XA 规范接口。
两阶段提交(Two-Phase Commit,简称2PC)是XA协议中的一种实现方式。通过接口规范,应用程序访问并使用RM的资源,并通过TM的事务接口(TX interface)定义需要执行的事务操作,然后 TM和 RM 会基于 XA 规范,执行二阶段提交协议进行事务的提交/回滚。
两阶段提交 2PC
阶段提交(Two-Phase Commit,简称2PC)是分布式事务处理中的一种算法,是最为经典的分布式事务解决方案之一。用于确保在涉及多个节点(或进程)的事务中,所有节点要么全部提交(commit),要么全部回滚(rollback),从而保持数据的一致性。
为了协调分布式环境下的不同服务,它通过一个中心协调器来协调多个参与者的事务。该协调器在第一阶段(准备阶段)询问所有参与者是否可以提交事务,如果所有参与者都准备好了,则在第二阶段(提交阶段)通知所有参与者提交事务。
两阶段提交算法是由Jim Gray和Andreas Reuter在1981年提出的,并在1983年发表了相关论文《事务处理:概念和技术》(Transaction Processing: Concepts and Techniques)中进行了详细讨论。这篇论文是分布式事务处理领域的经典著作之一,对于今天的分布式系统设计仍然具有重要的参考价值。
两阶段过程
两阶段指的是分布式事务的提交过程分为两个阶段,具体流程如下:
第一阶段(投票阶段):
协调者(Coordinator)向参与者(Participant)发出请求执行事务的消息。 参与者执行事务,并将 Undo 和 Redo 信息记录在事务日志中,但并不提交事务,也就是说此时资源已经被锁定。 参与者向协调者发送“投票”消息,表示事务是否执行成功。如果参与者执行成功,则返回“同意(Agree)”消息;如果参与者执行失败,则返回“否决(Abort)”消息。
第二阶段(提交阶段):
协调者收到所有参与者的投票信息,如果所有参与者都返回“同意”消息,则协调者向所有参与者发送“提交(Commit)”消息。 参与者收到“提交”消息后,执行提交操作,并释放在第一阶段中申请的所有资源。 如果任何一个参与者返回“否决”消息,协调者将向所有参与者发送“回滚(Rollback)”消息。 参与者收到“回滚”消息后,执行回滚操作,并释放在第一阶段中申请的所有资源。
需要注意的是,两阶段提交协议有可能会存在“阻塞”的问题,也就是说,在第一阶段中,如果有任何一个参与者无法响应,那么协调者将一直等待,直到超时。在这种情况下,需要采取超时机制和其他优化手段来提高系统的可用性和性能。
因为它需要在所有两阶段的缺点是存在单点故障和阻塞问题。
前提条件
两阶段提交的成立是有前提条件的。
必须假设网络短时间内可靠。即提交阶段不会丢失消息,同时也假设网络通信在全过程都不会出现误差,可以丢失消息,但不会传递错误的消息。两段式提交中投票阶段失败了可以回滚,而提交阶段失败了无法改变已提交的结果,因而此阶段耗时应尽可能短,这也是为了尽量控制网络风险的考虑。 必须假设故障节点最终能够恢复,不会永久性地处于失联状态。由于在准备阶段已经写入了完整的 Undo log,所以当失联机器一旦恢复,就能够从日志中找出已准备妥当但并未提交的事务数据,并向协调者查询该事务的状态,确定下一步应该进行提交还是回滚操作。
两阶段的问题
虽然两阶段可以解决大部分场景下的事务的一致性问题,并且原理简单,但是它存在以下一些缺点。
1.资源占用导致同步阻塞问题:在第一阶段投票之后,参与的事务的每个节点资源都被锁定,并且在第二阶段参与者等到协调者的响应才能继续执行,事务时间越长资源占用时间越长。并且如果协调者发生故障,参与者会一直等待其响应,这回导致整个系统的性能受到影响。
2.单点故障问题:在2PC中,协调者扮演着关键的角色,如果协调者发生故障,整个事务就会失败。协调者等待参与者回复时可以有超时机制,允许参与者宕机,但参与者等待协调者指令时无法做超时处理,事务将长时间持续。因此,协调者成为了系统的单点故障,这会影响整个系统的可用性。
3.数据不一致问题:其实两阶段提交有个前提,就是网络状况短时间内稳定,这样可以保证第一阶段投票完毕后第二个阶段可以顺利提交。并且必须假设因故障而下线的节点最终能宕机恢复,在第一阶段中可以有 Undo Log 可以保证。但是如果在第一阶段之后,协调者向参与者发送的提交请求丢失或者超时或者节点宕机,那么就会导致一些参与者已经提交了数据,而另外一些参与者却没有提交数据,从而导致数据不一致。
4.可扩展性问题:在大规模分布式系统中,参与者数量可能会非常庞大,这会导致协调者需要维护大量的状态信息,从而影响系统的可扩展性。
三阶段提交 3PC
三阶段提交(Three-Phase Commit,简称3PC)是分布式事务处理中的一种算法,是在两阶段提交(2PC)的基础上进一步发展的,主要是为了解决2PC中存在的一些问题,比如资源占用、同步阻塞、单点故障以及提交阶段可能出现的数据不一致问题。
相对于2PC,3PC 有两个变动的地方:
将第一阶段投票阶段拆分为CanCommit和PreCommit阶段:这样的好处是可以预先在资源没有锁定的情况下检查资源的可用情况,检查通过后PreCommit阶段再锁定资源。如果CanCommit的检查全部通过,这样后续成功的概率也会提高了。 引入了超时机制:之前只有参与者可以超时,在3PC中协调者和参与者都引入了超时机制。
三阶段过程
3阶段提交的具体过程如下:
第一阶段:准备阶段(CanCommit)
事务协调者向所有参与者发送准备请求,并询问参与者是否可以提交事务,并开始等待各参与者响应。参与者接收到请求后,会查询本地资源是否可以提交,并返回查询结果,而不需要对资源进行实际的修改操作。如果查询资源可以提交,则回复事务管理器“可以提交(Yes)”,否则回复“不可以提交(No)”。
和2PC的第一阶段不同的是,这里只执行检查,并不锁定资源。
第二阶段:预提交阶段(PreCommit)
协调者根据第一阶段参与者返回的结果,会出现两种情况:执行事务预提交和中断事务。
情况一,执行事务预提交:
所有参与者返回的都是Yes响应,协调者向参与者发送PreCommit预提交请求。 参与者收到预提交请求后执行事务操作,并且记录UndoLog和RedoLog。 各个参与者返回预提交的结果给协调者,成功返回Commit提交,失败返回Abort中止。
情况二,中断事务:
如果有任何一个参与者再阶段一返回了NO,或者接口相应超时,那么事务将中断。 协调者向各个参与者发送中断请求信息。 如果各个参与者收到请求,事务中断。如果没有收到请求,则按照超时处理,事务依旧中断。
第三阶段:确认阶段(DoCommit)
根据第二阶段的结果,也可以分为两种情况,提交事务和中断事务。
情况一,提交事务:
再PreCommit阶段,所有参与者返回Commit提交信息,协调者将会从预提交转换为提交状态,并向所有参与者发送doCommit请求。 参与者接收到doCommit请求后,会正式执行事务操作,最终提交事务释放资源占用。 参与者完成事务提交后向协调者发送ack响应,协调者收到所有参与者反馈的ack后,完成事务。 这一阶段有可能出现没有收到DoCommit请求,参与者会自动提交。
情况二,中断事务
在前一个阶段,如果有参与者返回Abort或者相应超时,那么事务中断,协调者向所有参与者发送Abort中断请求。 参与者收到请求Abort后,依赖 Undo Log 执行事务回滚,释放锁定的资源,并且向协调者返回Ack,反馈回滚结果。 协调者收到所有参与者的回滚Ack后,中断事务。 这个阶段有可能部分参与者返回回滚结果超时,或者没有收到回滚请求(发生不一致)。
解决了2PC哪些问题
3PC解决了2PC存在的以下问题:
单点故障:3PC引入了一个准备阶段,这样即使协调者在第一阶段失败,参与者也可以在第二阶段中完成提交或回滚,并且如果协调者故障参与者会自动提交,不需要一直等待。 性能问题:在3PC中,可以预先检查参与者状态,减少锁定资源的情况,提高系统的性能。 阻塞问题:3PC中通过在第一阶段中引入超时机制,避免了协调者一直等待的问题。如果协调者在一定时间内没有收到所有参与者的响应,就会继续进行第三阶段的操作,从而避免了事务的阻塞问题。 部分解决数据不一致问题:在3PC中,如果在第二阶段中协调者发生故障,参与者会继续等待协调者的恢复。如果协调者无法恢复,参与者会在一定时间内自行决定提交或回滚。这样可以部分避免2PC中的数据不一致问题。
只能说3PC对2PC存在的一些问题有改善,但没有彻底解决。
比如它依旧存在一致性风险问题,并且风险反而略有增加。进入 PreCommit 阶段之后,协调者发出的指令不是 Ack 而是 Abort,而此时因网络问题,有部分参与者直至超时都未能收到协调者的 Abort 指令的话,这些参与者将会错误地提交事务,这就产生了不同参与者之间数据不一致的问题。
由于3PC非常难实现,目前市面上主流的分布式事务解决方案都是2PC协议。所以,在实际应用中需要根据具体的场景和需求,选择适合的分布式事务协议。