分布式事务的几种解决方式

分布式事务指事务的操作位于不同的节点上,需要保证事务的AICD特性

1、 两阶段提交方案/XA方案

两阶段提交,通过引入协调者,来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。

运行过程

(1) 准备阶段

协调者询问参与者事务是否执行成功,参与者发回事务执行结果

分布式事务的5种解决方案_zookeeper

(2) 提交阶段

如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。

需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。

分布式事务的5种解决方案_回滚_02

存在的问题:

(1) 同步阻塞:所有事务参与者在等待其他参与者响应的时候都处于同步阻塞状态,无法进行其他操作。

(2) 单点问题:协调者在方案中起到非常大的作用,发生故障将会造成很大的影响,特别是在二阶段发生故障,所有参与者会一直等待状态,无法完成其他操作。

(3) 数据不一致:在阶段二,如果协调者只发送了部分Commit消息,此时网络发生异常,那么只有部分参与者接收到commit消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。

(4) 太过保守:任意一个节点失败就会导致整个事务失败,没有完善的容错机制。

这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景,如果要使用,那么基于Spring+JTA就可以搞定。

这个方案很少用,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规矩的。现在为服务,一般来说我们的规定和规范,是要求说每个服务只能操作自己对应的一个数据库。

如果你要操作别的服务对应的库,不允许直连别的服务的库,违反微服务架构的规范,你随便交叉胡乱访问,几百个服务的话,全体乱套,这样的一套服务是没法管理的,没法治理的,经常数据被别人改错,自己的数据库又被别人写挂。

2、 补偿事务(TCC)

TCC的全称是:Try、Confirm、Cancel

这个其实是用了补偿的概念,分为了三个阶段:

(1) Try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留

(2) Confirm阶段:这个阶段说的是在各个服务中执行实际的操作

(3) Cancel阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作,插入的要删除,删除要插入,修改了的改回来。

举个例子,假入 Bob 要向 Smith 转账,思路大概是:我们有一个本地方法,里面依次调用

(1) 首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。

(2) 在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。

(3) 如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。

这种方案几乎很少人使用,因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常恶心,在一些场景中,一些业务流程可能用TCC不太好定义及处理。

比较适合的场景

这个就是除非你是真的一致性要求太高,是你系统中核心之核心的场景,比如常见的就是资金类的场景,那你可以用TCC方案了,自己编写大量的业务逻辑,自己判断一个事务中的各个环节是否OK,不OK就执行补偿/回滚代码,而且最好是你的各个业务执行的时间都比较短。

比如说:一般跟钱相关的,支付、交易的场景,可以使用TCC,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性。

3、 本地消息表(异步确保)

本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路是来源于ebay。

基本思路就是:

消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。

消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。

生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

这种方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。

优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。在 .NET中 有现成的解决方案。

缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

4、 可靠消息最终一致性方案(MQ事务消息)

这个就是干脆不用本地消息表了,直接基于MQ来实现事务,比如RocketMQ就支持消息事务
大概实现:

(1) A系统先发送一个prepared消息到MQ,如果这个prepared消息发送失败那么就直接取消操作别执行了。

(2) 如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉MQ发送确认消息,如果失败就告诉MQ回滚消息。

(3) 如果发送了确认消息,那么此时B系统会接收到确认消息,然后执行本地的事务。

(4) MQ会自动定时轮询所有prepared消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所以没有发送确认消息?那么是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,别确认消息发送失败了。

(5) 这个方案里,要是系统B的事务失败了咋办?重试喽,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如B系统本地回滚后,想办法通知系统A也回滚(可以使用zookeeper的通知监听机制来进行通知),或者是发送报警由人工来手工回滚和补偿。

5、 最大努力通知方案

(1) 系统A本地事务执行完之后,发送个消息到MQ

(2) 这里会有个专门消费MQ的最大努力通知服务,这个服务会消费MQ然后写入数据库中记录下来,或者是放入内存队列也可以,接着调用系统B的接口。

(3) 要是系统B执行成功就ok了,要是系统B执行失败了,那么最大努力通知服务就定时尝试重新调用系统B,反复N次后,最后还是不行就放弃。