目录
- 1 DTP模型
- 2 2PC
- 2.1 方案简介
- 2.2 处理流程
- 2.2.1 阶段1:准备阶段
- 2.2.2 阶段2:提交阶段
- 2.3 方案总结
- 3 3PC
- 3.1 方案简介
- 3.2 处理流程
- 3.2.1 阶段1:canCommit
- 3.2.2 阶段2:preCommit
- 3.3.3 阶段3:do Commit
- 3.3 方案总结
- 4 TCC
- 4.1 方案简介
- 4.2 处理流程
- 4.2.1 阶段1:Try 阶段
- 4.2.2 阶段2:Confirm / Cancel 阶段
- 4.3 方案总结
- 5 本地消息表
- 5.1 方案简介
- 5.2 处理流程
- 5.3 方案总结
- 6 MQ事务
- 6.1 方案简介
- 6.2 处理流程
- 6.2.1 正常情况——事务主动方发消息
- 6.2.2 异常情况——事务主动方消息恢复
- 6.3 方案总结
1 DTP模型
维基百科:https://zh.wikipedia.org/wiki/X/Open_XA
分布式事务解决方案几乎都是柔性事务,分布式事务的实现有许多种,其中较经典是由Tuxedo提出的XA分布式事务协议,XA协议包含二阶段提交(2PC)和三阶段提交(3PC)两种实现。
其他还有 TCC、MQ 等最终一致性解决方案,至于工作中用哪种方案,需要根据业务场景选取,2PC/3PC、TCC
数据强一致性高,而MQ是最终数据一致。
https://www.ibm.com/docs/zh/db2/10.5?topic=managers-designing-xa-compliant-transaction
X/Open DTP(X/Open Distributed Transaction Processing Reference Model) 是X/Open 这个组织定义的一套分布式事务的标准,也就是了定义了规范和API接口,由厂商进行具体的实现
X/Open DTP中的角色
AP(Application Program):应用程序,主要是定义事务边界以及那些组成事务的特定于应用程序的操作。
RM(Resouces Manager):资源管理器,管理一些共享资源的自治域,如提供对诸如数据库之类的共享资源的访问。譬如:数据库、文件系统等,并且提供了这些资源的访问方式。
TM(Transaction Manager):事务管理器,管理全局事务,协调事务的提交或者回滚,并协调故障恢复。
DTP模型里面定义了XA协议接口,TM 和 RM 通过XA接口进行双向通信
2 2PC
2PC
、3PC
,都是基于XA
协议的
2.1 方案简介
二阶段提交协议(Two-phase Commit,即2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。事务的发起者称协调者,事务的执行者称参与者。
在分布式系统里,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。当一个事务跨多个节点时,为了保持事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚(rollback)。
二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
核心思想就是对每一个事务都采用先尝试后提交的处理方式,处理后所有的读操作都要能获得最新的数据,因此也可以将二阶段提交看作是一个强一致性算法。
2.2 处理流程
简单一点理解,可以把协调者节点比喻为带头大哥,参与者理解比喻为跟班小弟,带头大哥统一协调跟班小弟的任务执行。
2.2.1 阶段1:准备阶段
- 1、协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。
- 2、各参与者执行事务操作,将undo和redo信息记入事务日志中(但不提交事务)。
- 3、如参与者执行成功,给协调者反馈yes,即可以提交;如执行失败,给协调者反馈no,即不可提交。
2.2.2 阶段2:提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源) 接下来分两种情况分别讨论提交阶段的过程。
情况1,当所有参与者均反馈yes,提交事务:
- 1、协调者向所有参与者发出正式提交事务的请求(即commit请求)。
- 2、参与者执行commit请求,并释放整个事务期间占用的资源。
- 3、各参与者向协调者反馈ack(应答)完成的消息。
- 4、协调者收到所有参与者反馈的ack消息后,即完成事务提交。
情况2,当任何阶段1一个参与者反馈no,中断事务:
- 1、协调者向所有参与者发出回滚请求(即rollback请求)。
- 2、参与者使用阶段1中的undo信息执行回滚操作,并释放整个事务期间占用的资源。
- 3、各参与者向协调者反馈ack完成的消息。
- 4、协调者收到所有参与者反馈的ack消息后,即完成事务中断。
2.3 方案总结
2PC是一个强一致性的同步阻塞协议,事务执⾏过程中需要将所需资源全部锁定,也就是俗称的 刚性事务
2PC方案实现起来简单,实际项目中使用比较少,主要因为以下问题:
- 性能问题 所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
- 可靠性问题 如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。
- 数据一致性问题 在阶段2中,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
3 3PC
3.1 方案简介
三阶段提交协议,是二阶段提交协议的改进版本,与二阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制(2PC
中只有协调者有超时机制)。
三阶段提交将二阶段的准备阶段拆分为2个阶段,插入了一个preCommit阶段,使得原先在二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。
3.2 处理流程
3.2.1 阶段1:canCommit
协调者向参与者发送canCommit请求,参与者如果可以提交就返回yes响应(参与者不执行事务操作),否则返回no响应:
- 1、协调者向所有参与者发出包含事务内容的canCommit请求,询问是否可以提交事务,并等待所有参与者答复。
- 2、参与者收到canCommit请求后,如果认为可以执行事务操作,则反馈yes并进入预备状态,否则反馈no。
3.2.2 阶段2:preCommit
协调者根据阶段1 canCommit参与者的反应情况来决定是否可以基于事务的preCommit操作。根据响应情况,有以下两种可能。
情况1,阶段1所有参与者均反馈yes,参与者预执行事务:
- 1、协调者向所有参与者发出preCommit请求,进入准备阶段。
- 2、参与者收到preCommit请求后,执行事务操作,将undo和redo信息记入事务日志中(但不提交事务)。
- 3、各参与者向协调者反馈ack响应或no响应,并等待最终指令。
情况2,阶段1任何一个参与者反馈no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务:
- 1、协调者向所有参与者发出abort请求。
- 2、无论收到协调者发出的abort请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。
3.3.3 阶段3:do Commit
该阶段进行真正的事务提交,也可以分为以下两种情况:
情况1:阶段2所有参与者均反馈ack响应,执行真正的事务提交:
- 1、如果协调者处于工作状态,则向所有参与者发出do Commit请求。
- 2、参与者收到do Commit请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
- 3、各参与者向协调者反馈ack完成的消息。
- 4、协调者收到所有参与者反馈的ack消息后,即完成事务提交。
阶段2任何一个参与者反馈no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务:
- 1、如果协调者处于工作状态,向所有参与者发出abort请求。
- 2、参与者使用阶段1中的undo信息执行回滚操作,并释放整个事务期间占用的资源。
- 3、各参与者向协调者反馈ack完成的消息。
- 4、协调者收到所有参与者反馈的ack消息后,即完成事务中断
注意:进入阶段3后,如果协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的do Commit请求或rollback请求。此时,参与者都会在等待超时之后,继续执行事务提交。
阶段三 只允许成功不允许失败,如果服务器宕机或者停电,因为记录的阶段二的数据,重启服务后在提交事务,所以,到了阶段三,失败了也不进行回滚,只允许成功。
3.3 方案总结
优点
xxxxxxxxxx
相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段3中协调者出现问题时,参与者会继续提交事务。
缺点
xxxxxxxxxx
数据不一致问题依然存在,当在参与者收到preCommit请求后等待do commite指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
4 TCC
4.1 方案简介
TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。
TCC是服务化的二阶段编程模型,其Try、Confirm、Cancel 3个方法均由业务编码实现,基本类似两阶段提交
- Try操作作为一阶段,负责资源的检查和预留。
- Confirm操作作为二阶段提交操作,执行真正的业务。
- Cancel是预留资源的取消。
TCC事务的Try、Confirm、Cancel可以理解为SQL事务中的Lock、Commit、Rollback。
TCC
为在业务层编写代码实现的两阶段提交。TCC
分别指 Try
、Confirm
、Cancel
,一个业务操作要对应的写这三个方法。
Github 上有诸多具体的实现,例如 https://github.com/changmingxie/tcc-transaction
4.2 处理流程
为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建2个步骤,库存服务和订单服务分别在不同的服务器节点上。
4.2.1 阶段1:Try 阶段
从执行阶段来看,与传统事务机制中业务逻辑相同。但从业务角度来看,却不一样。TCC机制中的Try仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:
- 完成所有业务检查( 一致性 )
- 预留必须业务资源( 准隔离性 )
- Try 尝试执行业务 TCC事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
假设商品库存为100,购买数量为2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。
4.2.2 阶段2:Confirm / Cancel 阶段
根据Try阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。 Confirm和Cancel操作满足幂等性,如果Confirm或Cancel操作执行失败,将会不断重试直到执行完成。
Confirm:确认
当Try阶段服务全部正常执行, 执行确认业务逻辑操作
这里使用的资源一定是Try阶段预留的业务资源。在TCC事务机制中认为,如果在Try阶段能正常的预留资源,那Confirm一定能完整正确的提交。Confirm阶段也可以看成是对Try阶段的一个补充,Try+Confirm一起组成了一个完整的业务逻辑。
Cancel:取消
当Try阶段存在服务执行失败, 进入Cancel阶段
Cancel取消执行,释放Try阶段预留的业务资源,上面的例子中,Cancel操作会把冻结的库存释放,并更新订单状态为取消。
4.3 方案总结
TCC事务机制相对于传统事务机制(X/Open XA),TCC事务机制相比于上面介绍的XA事务机制,有以下优点:
- 性能提升 具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
- 数据最终一致性 基于Confirm和Cancel的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
- 可靠性 解决了XA协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
缺点:
TCC的Try、Confirm和Cancel操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
5 本地消息表
5.1 方案简介
本地消息表的方案最初是由ebay提出,核心思路是将分布式事务拆分成本地事务进行处理。
方案通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
这样设计可以避免”业务处理成功 + 事务消息发送失败“,或”业务处理失败 + 事务消息发送成功“的棘手情况出现,保证2个系统事务的数据一致性。
5.2 处理流程
下面把分布式事务最先开始处理的事务方成为事务主动方,在事务主动方之后处理的业务内的其他事务成为事务被动方。
为了方便理解,下面继续以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建2个步骤,库存服务和订单服务分别在不同的服务器节点上,其中库存服务是事务主动方,订单服务是事务被动方。
事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。
整个业务处理流程如下:
步骤1 事务主动方处理本地事务。 事务主动方在本地事务中处理业务更新操作和写消息表操作。 上面例子中库存服务阶段在本地事务中完成扣减库存和写消息表(图中1、2)。
步骤2 事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。 消息中间件可以基于Kafka、RocketMQ消息队列,事务主动方法主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。 上面例子中,库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中3 - 5)。
步骤3 事务被动方通过消息中间件,通知事务主动方事务已处理的消息。 上面例子中,订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中6 - 8)
为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等。具体保存一致性的容错处理如下:
1、当步骤1处理出错,事务回滚,相当于什么都没发生。
2、当步骤2、步骤3处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮询为超时消息数据,再次发送的消息中间件进行处理。事务被动方消费事务消息重试处理。
3、如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。
4、如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。
5.3 方案总结
方案的优点如下:
- 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对MQ中间件特性的依赖。
- 方案轻量,容易实现。
缺点如下:
- 与具体的业务场景绑定,耦合性强,不可共用。
- 消息数据与业务数据同库,占用业务系统资源。
- 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限
6 MQ事务
MQ事务保证最终一致性。
6.1 方案简介
基于MQ的分布式事务方案其实是对本地消息表的封装,将本地消息表存于MQ 内部,其他方面的协议基本与本地消息表一致。
6.2 处理流程
下面主要基于RocketMQ4.3之后的版本介绍MQ的分布式事务方案。
在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ的事务消息相对于普通MQ,相对于提供了2PC的提交接口,方案如下:
6.2.1 正常情况——事务主动方发消息
这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:
1、发送方向 MQ服务端(MQ Server)发送half消息。
2、MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功。
3、发送方开始执行本地事务逻辑。
4、发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
5、MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。
6.2.2 异常情况——事务主动方消息恢复
在断网或者应用重启等异常情况下,图中第4步提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:
- 5、MQ Server 对该消息发起消息回查。
- 6、发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 7、发送方根据检查得到的本地事务的最终状态再次提交二次确认
- 8、MQ Server基于commit / rollback 对消息进行投递或者删除
介绍完RocketMQ的事务消息方案后,由于前面已经介绍过本地消息表方案,这里就简单介绍RocketMQ分布式事务:
事务主动方基于MQ通信通知事务被动方处理事务,事务被动方基于MQ返回处理结果。 如果事务被动方消费消息异常,需要不断重试,业务处理逻辑需要保证幂等。 如果是事务被动方业务上的处理失败,可以通过MQ通知事务主动方进行补偿或者事务回滚。
6.3 方案总结
相比本地消息表方案,MQ事务方案优点是:
- 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
- 吞吐量优于使用本地消息表方案。
缺点是:
- 一次消息发送需要两次网络请求(half消息 + commit/rollback消息)
- 业务处理服务需要实现消息状态回查接口