分布式事务的实现方式
1、二阶段提交(2PC)
二阶段提交(2PC)是分布式事务中最强大的事务类型之一,二阶段提交就是分两个阶段提交,第一阶段询问各个事务数据源是否准备好,第二阶段才是真正将数据提交给事务数据源。为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被成为参与者(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。
1.1、工作流程
(一)第一阶段
(1)协调者会问所有的参与者结点,是否可以执行提交操作。
(2)各个参与者开始事务执行的准备工作:如:资源上锁,预留资源。
(3)参与者响应协调者,如果事务的准备工作成功,则回应“可以提交”,否则回应“拒绝提交”。
(二)第二阶段
(1)如果所有的参与者都回应“可以提交”,那么,协调者向所有的参与者发送“正式提交”的命令。参与者完成正式提交,并释放所有资源,然后回应“完成”,协调者收集各结点的“完成”回应后结束这个Global Transaction。
(2)如果有一个参与者回应“拒绝提交”,那么,协调者向所有的参与者发送“回滚操作”,并释放所有资源,然后回应“回滚完成”,协调者收集各结点的“回滚”回应后,取消这个Global Transaction。
1.2、缺陷
(1)同步阻塞:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
(2)单点故障:由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)。
(3)数据不一致:在二阶段提交的第二阶段中,当协调者向参与者发送Commit请求之后,发生了局部网络异常或者在发送Commit请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了Commit请求。而在这部分参与者接到Commit请求之后就会执行Commit操作。但是其他部分未接到Commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
(4)二阶段提交无法解决的问题:协调者再发出Commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
2、三阶段提交(3PC)
2.1、相比2PC改变
(1)引入超时机制:同时在协调者和参与者中都引入超时机制。
(2)在第一阶段和第二阶段中插入一个准备阶段:保证了在最后提交阶段之前各参与节点的状态是一致的。也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
2.2、工作流程
(一)CanCommit阶段
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送Commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
(1)协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
(2)参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No。
(二)PreCommit阶段
协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能。
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
(1)协调者向参与者发送PreCommit请求,并进入Prepared阶段。
(2)参与者接收到PreCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中。
(3)如果参与者成功的执行了事务操作,则返回Ack响应,同时开始等待最终指令。
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
(1)协调者向所有参与者发送Abort请求。
(2)参与者收到来自协调者的Abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
(三)doCommit阶段
该阶段进行真正的事务提交,也可以分为以下两种情况。
执行提交
(1)协调接收到参与者发送的Ack响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送DoCommit请求。
(2)参与者接收到DoCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
(3)事务提交完之后,向协调者发送Ack响应。
(4)协调者接收到所有参与者的Ack响应之后,完成事务。
中断事务
(1)协调者向所有参与者发Abort请求。
(2)参与者接收到Abort请求之后,利用其在阶段二记录的Undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
(3)参与者完成事务回滚之后,向协调者发送Ack消息。
(4)协调者接收到参与者反馈的Ack消息之后,执行事务的中断。
2.3、解决问题
(1)单点故障问题。
(2)减少阻塞。
提示:一旦参与者无法及时收到来自协调者的信息之后,会默认执行Commit,而不会一直持有事务资源并处于阻塞状态
2.4、缺陷
(1)一致性问题:由于网络原因,协调者发送的Abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了Commit操作。这样就和其他接到Abort命令并执行回滚的参与者之间存在数据不一致的情况 。
3、TCC(Try Confirm Cancel)
3.1、概念
一个完整的TCC业务由一个主业务服务和若干个从业务服务组成,主业务服务发起并完成整个业务活动,TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
(1)Try:完成所有业务检查,预留必须业务资源。
(2)Confirm:真正执行业务,不作任何业务检查;只使用Try阶段预留的业务资源;Confirm操作满足幂等性。
(3)Cancel:释放Try阶段预留的业务资源;Cancel操作满足幂等性。
3.2、工作流程
(1)第一阶段:主业务服务分别调用所有从业务的Try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的Try操作都调用成功或者某个从业务服务的Try操作失败,进入第二阶段。
(2)第二阶段:活动管理器根据第一阶段的执行结果来执行Confirm或Cancel操作。如果第一阶段所有Try操作都成功,则活动管理器调用所有从业务活动的Confirm操作。否则调用所有从业务服务的Cancel操作。
3.3、与2PC比较
(1)位于业务服务层而非资源层。
(2)没有单独的准备(Prepare)阶段,Try操作兼备资源操作与准备能力。
(3)Try操作可以灵活选择业务资源的锁定粒度。
(4)开发成本较高。
(5)XA是资源层面的分布式事务,强一致性,在而阶段提交的整个过程中,一直会持有资源的锁。 TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。
3.4、缺陷
(1)Canfirm和Cancel的幂等性很难保证。
(2)这种方式缺点比较多,通常在复杂场景下是不推荐使用的,除非是非常简单的场景,非常容易提供回滚Cancel,而且依赖的服务也非常少的情况。
(3)这种实现方式会造成代码量庞大,耦合性高。而且非常有局限性,因为有很多的业务是无法很简单的实现回滚的,如果串行的服务很多,回滚的成本实在太高。
4、本地消息表
4.1、基本思路
(1)消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
(2)消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
(3)生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
4.2、优缺点
(1)优点:一种非常经典的实现,避免了分布式事务,实现了最终一致性。
(2)缺点:消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
5、MQ事务消息(比如RocketMQ)
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
5.1、基本思路
(1)首先,发送一个事务消息,这个时候,RocketMQ将消息状态标记为Prepared,注意此时这条消息消费者是无法消费到的。
(2)接着,执行业务代码逻辑,可能是一个本地数据库事务操作。
(3)最后,确认发送消息,这个时候,RocketMQ将消息状态标记为可消费,这个时候消费者,才能真正的保证消费到这条数据。
如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事务消息,如果发现了Prepared消息,它会向消息发送端(生产者)确认。RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
5.2、优缺点
(1)优点:实现了最终一致性,不需要依赖本地数据库事务。
(2)缺点:实现难度大,主流MQ不支持,没有.NET客户端,RocketMQ事务消息部分代码也未开源。
6、MQ非事务消息(加独立消息服务、或者本地事务表)
另外一种实现,并不是所有的MQ都支持事务消息。也就是消息一旦发送到消息队列中,消费者立马就可以消费到。此时可以使用独立消息服务、或者本地事务表。
6.1、基本思路
(1)将消息先发送到一个我们自己编写的一个“独立消息服务”应用中,刚开始处于Prepare状态。
(2)业务逻辑处理成功后,确认发送消息,这个时候“独立消息服务”才会真正的把消息发送给消息队列。
(3)消费者消费成功后,Ack时,除了对消息队列进行Ack(图中没有画出),对于独立消息服务也要进行Ack,“独立消息服务”一般是把这条消息删除。而定时扫描Prepare状态的消息,向消息发送端(生产者)确认的工作也由独立消息服务来完成。
对于“本地事务表”,其实和“独立消息服务”的作用类似,只不过“独立消息服务”是需要独立部署的,而“本地事务表”是将“独立消息服务”的功能内嵌到应用中。
7、柔性事务:最大努力通知
最大努力通知型( Best-effort delivery)是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果 不影响主动方的处理结果。典型的使用场景:如银行通知、商户通知等。最大努力通知型的实现方案,一般符合以下特点:
(1) 不可靠消息:业务活动主动方,在完成业务处理之后,向业务活动的被动方发送消息,直到通知N次后不再通知,允许消息丢失(不可靠消息)。
(2)定期校对:业务活动的被动方,根据定时策略,向业务活动主动方查询(主动方提供查询接口),恢复丢失的业务消息。
8、Sagas长事务
在Sagas事务模型中,一个长事务是由一个预先定义好执行顺序的子事务集合和他们对应的补偿子事务集合组成的。典型的一个完整的交易由T1、T2、…、Tn等多个业务活动组成,每个业务活动可以是本地操作、或者是远程操作,所有的业务活动在Sagas事务下要么全部成功,要么全部回滚,不存在中间状态。
8.1、实现机制
(1)每个业务活动都是一个原子操作。
(2)每个业务活动均提供正反操作。
(3)任何一个业务活动发生错误,按照执行的反顺序,实时执行反操作,进行事务回滚。
(4)回滚失败情况下,需要记录待冲正事务日志,通过重试策略进行重试。
(5)冲正重试依然失败的场景,提供定时冲正服务器,对回滚失败的业务进行定时冲正。
(6)定时冲正依然失败的业务,等待人工干预。
8.2、优缺点
(1)优点:Sagas长事务模型支持对数据一致性要求比较高的场景比较适用,由于采用了补偿的机制,每个原子操作都是先执行任务,避免了长时间的资源锁定,能做到实时释放资源,性能相对有保障。
(2)缺点:Sagas长事务方式如果由业务去实现,复杂度与难度并存。