分布式事务 解决方案:2PC,TCC以及基于消息的最终一致性

分布式事务

什么是分布式事务?

分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
本质上来说分布式事务用于在分布式系统中保证不同节点之间的数据一致性。分布式事物的实现有很多种,最具代表性的是由Oracle Tuxedo系统提出的XA分布式事物协议。

解决方案

两阶段提交(2PC)

两阶段提交又称2PC(two-phase commit protocol),2pc是一个非常经典的强一致、中心化的原子提交协议。这里所说的中心化是指协议中有两类节点:一个是中心化协调者节点(coordinator)和N个参与者节点(partcipant)。
下面我们就以一个尽量贴近实际业务场景的操作来举例:“假设在一个分布式架构的系统中事务的发起者通过分布式事务协调者(如RocketMQ,在早期RocketMQ版本不提供事务消息特性时,有些公司会自己研发一个基于MQ的可靠消息服务来实现一定的分布式事务的特性)分别向应用服务A、应用服务B发起处理请求,二者在处理的过程中会分别操作自身服务的数据库,现在要求应用服务A、应用服务B的数据处理操作要在一个事务里”。
在上面这个例子中如果采用两阶段提交来实现分布式事务,那么其运行原理应该是个什么样的呢?(如????):

第一阶段:请求/表决阶段(点击放大)

分布式事务 解决方案:2PC,TCC以及基于消息的最终一致性

既然称为两阶段提交,说明在这个过程中是大致存在两个阶段的处理流程。第一个阶段如????图所示,这个阶段被称之为请求/表决阶段。是个什么意思呢?

就是在分布式事务的发起方在向分布式事务协调者(Coordinator)发送请求时,Coordinator首先会分别向参与者(Partcipant)节点A、参与这节点(Partcipant)节点B分别发送事务预处理请求,称之为Prepare,有些资料也叫"Vote Request"。

说的直白点就是问一下这些参与节点"这件事你们能不能处理成功了",此时这些参与者节点一般来说就会打开本地数据库事务,然后开始执行数据库本地事务,但在执行完成后并不会立马提交数据库本地事务,而是先向Coordinator报告说:“我这边可以处理了/我这边不能处理”。

如果所有的参与这节点都向协调者作了“Vote Commit”的反馈的话,那么此时流程就会进入第二个阶段了。

第二阶段:提交/执行阶段(正常流程)

分布式事务 解决方案:2PC,TCC以及基于消息的最终一致性

如果所有参与者节点都向协调者报告说“我这边可以处理”,那么此时协调者就会向所有参与者节点发送“全局提交确认通知(global_commit)”,即你们都可以进行本地事务提交了,此时参与者节点就会完成自身本地数据库事务的提交,并最终将提交结果回复“ack”消息给Coordinator,然后Coordinator就会向调用方返回分布式事务处理完成的结果。

第二阶段:提交/执行阶段(异常流程)

分布式事务 解决方案:2PC,TCC以及基于消息的最终一致性

相反,在第二阶段除了所有的参与者节点都反馈“我这边可以处理了”的情况外,也会有节点反馈说“我这边不能处理”的情况发生,此时参与者节点就会向协调者节点反馈“Vote_Abort”的消息。此时分布式事务协调者节点就会向所有的参与者节点发起事务回滚的消息(“global_rollback”),此时各个参与者节点就会回滚本地事务,释放资源,并且向协调者节点发送“ack”确认消息,协调者节点就会向调用方返回分布式事务处理失败的结果。

以上就是两阶段提交的基本过程了,那么按照这个两阶段提交协议,分布式系统的数据一致性问题就能得到满足吗?

实际上分布式事务是一件非常复杂的事情,两阶段提交只是通过增加了事务协调者(Coordinator)的角色来通过2个阶段的处理流程来解决分布式系统中一个事务需要跨多个服务节点的数据一致性问题。但是从异常情况上考虑,这个流程也并不是那么的无懈可击。

假设如果在第二个阶段中Coordinator在接收到Partcipant的"Vote_Request"后挂掉了或者网络出现了异常,那么此时Partcipant节点就会一直处于本地事务挂起的状态,从而长时间地占用资源。当然这种情况只会出现在极端情况下,然而作为一套健壮的软件系统而言,异常Case的处理才是真正考验方案正确性的地方。

以下几点是XA-两阶段提交协议中会遇到的一些问题:

性能问题。从流程上我们可以看得出,其最大缺点就在于它的执行过程中间,节点都处于阻塞状态。各个操作数据库的节点此时都占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。

协调者单点故障问题。事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,会导致参与者收不到提交或回滚的通知,从而导致参与者节点始终处于事务无法完成的中间状态。

丢失消息导致的数据不一致问题。在第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就会导致节点间数据的不一致问题。

补偿事务(TCC)

TCC(Try-Confirm-Cancel)又称补偿事务。它实际上与2PC、3PC一样,都是分布式事务的一种实现方案而已。它分为三个操作:
  Try 阶段:尝试执行,完成所有业务检查(一致性),预留必需业务资源(准隔离性)。
Confirm 阶段:确认真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作满足幂等性。要求具备幂等设计,Confirm 失败后需要进行重试。
Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源,Cancel 操作满足幂等性。Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。
举个简单的例子:如果你用 100 元买了一瓶水, Try 阶段:你需要向你的钱包检查是否够 100 元并锁住这 100 元,水也是一样的。

如果有一个失败,则进行 Cancel(释放这 100 元和这一瓶水),如果 Cancel 失败不论什么失败都进行重试 Cancel,所以需要保持幂等。

如果都成功,则进行 Confirm,确认这 100 元被扣,和这一瓶水被卖,如果 Confirm 失败无论什么失败则重试(会依靠活动日志进行重试)。

对于 TCC 来说适合一些:

强隔离性,严格一致性要求的活动业务。
执行时间较短的业务。

最终一致性

所谓的消息事务就是基于消息中间件的两阶段提交,本质上是中间件的一种特殊利用,他是将本地事务和发消息放在一个分布式事务里,保证要么本地操作成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:

分布式事务-消息事务

步骤如下:

1、:服务A向消息中间件发送一条预备消息。

2、消息中间件保存预备消息并返回成功。

3、服务A执行本地事务。

4、服务A发送提交消息给消息中间件,服务B接收到消息之后执行本地事务。

基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(服务A的本地操作+发消息)+服务B的本地操作,其中服务B的操作由消息驱动,只要消息事务成功,那么服务A一定成功,消息也一定发出来了,这时候服务B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到服务B操作成功,这样就变相地实现了A与B的分布式事务。

以上几个步骤可能存在异常情况,现在对其进行分析:

步骤一出错:则整个事务失败,不会执行服务A的本地操作。

步骤二出错:则整个事务失败,不会执行服务A的本地操作。

步骤三出错:需要做回滚预备消息,由服务A实现一个消息中间件的回调接口,消息中间件会不断执行回调接口,检查服务A事务执行是否执行成功,如果失败则回滚预备消息。

步骤四出错:这个时候服务A的本地事务是成功的,但是消息中间件不需要回滚,其实通过回调接口,消息中间件能够检查到服务A执行成功了,这个时候其实不需要服务发提交消息了,消息中间件可以自己对消息进行提交,从而完成整个消息事务。