如何解决分布式事务 | 🏆 技术专题第五期征文

845 阅读11分钟
	记得年初找工作的时候,面试官总会问做没做过分布式系统,作为一个开发人员,分布式成了
 标配,而分布式系统带来的分布式事务也成了标配了。如何解决分布式问题是被问到的最多的一个
 问题,由于并没有充分的准备,所以回答是总是摸不到头脑,所以借此机会,我通过查阅资料并结
 合自己的工作经验,总结了一些解决分布式事务的方案。
    我总结了五种解决方案,分别是XA方案、TCC方案、本地消息表、可靠消息最终一致性方案和
 最大努力通知方案,主要就方案的优缺点和主要原理进行展开。

一.XA 方案

	XA 方案,核心为两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)
 的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提
 交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。
	举个例子,比如说咱们公司里会组织团建,然后一般会有个团建负责人,一般团建负责人会提
 前一周问一下团队里的人,说,大家伙,下周六我们去滑雪+烧烤,去吗?这个时候团建负责人开
 始等待每个人的回答,如果所有人都说ok,那么就可以决定一起去这次团建。如果这个阶段里,任
 何一个人回答说,我有事不去了,那么团建负责人取消这次活动。
	这个就是所谓的XA事务,两个阶段提交,有一个事务管理器的概念,负责协调多个数据库(资
 源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复ok,那么
 就正式提交事务,在各个数据库上执行操作,如果任何一个数据库回答不ok,那么就回滚事务。
	这种分布式事务方案,比较适合单块应用里,跨多个分布式事务,而且因为严重依赖于数据库
 层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。
	一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的,现在微服务,一个大
 的系统分成几百个服务,几十个服务。一般来说,我们的规定和规范,是要求说每个服务职能
 操作自己对应的一个数据库。如果你要操作别的服务对应的库,不允许直连别的服务的库,违
 反微服务架构的规范,你随便交叉胡乱访问,几百个服务的话,全体乱套,这样的一套服务是
 没法管理的,没法治理的,经常数据被别人改错,自己的库被别人写挂。如果你要操作别人的
 服务的库,你必须是通过调用别的服务的接口来实现,绝对不允许交叉访问别人的数据库。

二.TCC 方案

	TCC 的全称是Try-Confirm-Cancel,可以理解为先尝试操作,又可以被称为两阶段补偿事
 务,第一阶段try只是预留资源,第二阶段要明确的告诉服务提供者,这个资源你到底要不要,对
 应第二阶段的confirm/cancel,用来清除第一阶段的影响,所以叫补偿型事务。
	举个例子,我们在网上购物的时候,在页面点了支付按钮,调用支付服务,那我们后台要实现
 下面三个步骤:
 首先修改订单状态,也就是try阶段:
    订单服务:修改订单的状态为支付中;
    账户服务:账户余额不变,可用余额减1,然后将1这个数字冻结在一个单独的字段里;
    库存服务:库存数量不变,可销售库存减1,然后将1这个数字冻结在一个单独的字段里;
 然后从账户中扣减金钱,也就是confirm阶段:
    订单服务:修改订单的状态为支付完成;
    账户服务:账户余额变为(当前值减冻结字段的值),可用余额不变(Try阶段减过了),冻结字段
    清0;
    库存服务:库存变为(当前值减冻结字段的值),可销售库存不变(Try阶段减过了),冻结字段
    清0;
 最后从库存中扣减库存,也就是cancel阶段:
    订单服务:修改订单的状态为未支付
    账户服务:账户余额不变,可用余额变为(当前值加冻结字段的值),冻结字段清0。
    库存服务:库存不变,可销售库存变为(当前值加冻结字段的值),冻结字段清0。
以此来达到事务的效果,要么一起成功,要么一起失败,这就是TCC分布式事务方案。
	这种方案说实话几乎很少人使用,我用的也比较少,因为这个事务回滚实际上是严重依赖于你
自己写代码来回滚和补偿了,会造成补偿代码巨大。但是也有使用的场景,一般来说跟钱相关
的,跟钱打交道的,支付、交易相关的场景,都会用 TCC,严格保证分布式事务,要么全部成
功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。但是最好是你
的各个阶段执行的时间都比较短。但是说实话,一般尽量别这么搞,自己手写回滚逻辑,或者
是补偿逻辑,代码很复杂,而且那个业务代码是很难维护的。
    

三、本地消息表

	这种实现方式的思路,是国外的 ebay 搞出来的这么一套思想,后来通过支付宝等公司的布
道,在业内广泛使用。其基本的设计思想是将远程分布式事务拆分成一系列的本地事务。如果
不考虑性能及设计优雅,借助关系型数据库中的表即可实现。
	本地消息表的大概意思是这样的:
    1A 系统在自己本地一个事务里操作同时,插入一条数据到消息表;
    2、接着 A 系统将这个消息发送到 MQ 中去;
    3B 系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其
    他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复
    处理消息;
    4B 系统执行成功之后,就会更新自己本地消息表的状态以及 A 系统消息表的状态;
    5、如果 B 系统处理失败了,那么就不会更新消息表状态,那么此时 A 系统会定时扫描自己
    的消息表,如果有未处理的消息,会再次发送到 MQ 中去,让 B 再次处理;
    6、这个方案保证了最终一致性,哪怕 B 事务失败了,但是 A 会不断重发消息,直到 B 那
    边成功为止。
	举个经典的跨行转账的例子,第一步A账户扣款1w,通过本地事务保证了凭证消息插入到消息
表中,第二步通知对方银行账户上加 1W 了。那问题来了,如何通知到对方呢?通常采用两种方式:
    1.采用时效性高的 MQ,由对方订阅消息并监听,有消息时自动触发事件;
    2.采用定时轮询扫描的方式,去检查消息表的数据。
	两种方式其实各有利弊,仅仅依靠 MQ,可能会出现通知失败的问题。而过于频繁的定时轮
询,效率也不是最佳的(90% 是无用功)。所以,我们一般会把两种方式结合起来使用。
	解决了通知的问题,又有新的问题了。万一这消息有重复被消费,往用户帐号上多加了钱,那
岂不是后果很严重?仔细思考,其实我们可以消息消费方,也通过一个“消费状态表”来记录消费状
态。在执行“加款”操作之前,检测下该消息(提供标识)是否已经消费过,消费完成后,通过本地
事务控制来更新这个“消费状态表”。这样子就避免重复消费的问题。	上诉的方式是一种非常经典的
实现,基本避免了分布式事务,实现了“最终一致性”。但是,关系型数据库的吞吐量和性能方面存
在瓶颈,频繁的读写消息会给数据库造成压力。所以,在真正的高并发场景下,该方案也会有瓶颈
和限制的。 
    

四、可靠消息最终一致性方案

	这个方案的意思,就是干脆不要用本地的消息表了,直接基于 MQ 来实现事务。比如阿里的
RocketMQ就支持消息事务。大概的意思就是:
    1A 系统先发送一个 prepared 消息到 mq,如果这个 prepared 消息发送失败那么就
    直接取消操作别执行了;
    2、如果这个消息发送成功过了,那么接着执行本地事务,如果成功就告诉 mq 发送确认消
    息,如果失败就告诉 mq 回滚消息;
    3、如果发送了确认消息,那么此时 B 系统会接收到确认消息,然后执行本地的事务;
    4、mq 会自动定时轮询所有 prepared 消息回调你的接口,问你,这个消息是不是本地事务
    处理失败了,所有没发送确认的消息,是继续重试还是回滚?一般来说这里你就可以查下数据
    库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执
    行成功了,而确认消息却发送失败了;
    5、这个方案里,要是系统 B 的事务失败了咋办?会自动不断重试直到成功,如果实在是不
    行,要么就是针对重要的资金类业务进行回滚,比如 B 系统本地回滚后,想办法通知系统 A
    也回滚;或者是发送报警由人工来手工回滚和补偿;
可靠消息最终一致性就是保证消息从生产方经过消息中间件传递到消费方的一致性,使用RocketMQ
主要解决了两个功能:
    1、本地事务与消息发送的原子性问题。
    2、事务参与方接收消息的可靠性。
	可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的
事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现
了两个服务的解耦。这个方案还是比较合适的,目前国内互联网公司大都是这么用的。
    

5、最大努力通知方案

这个方案的大致意思就是:
    系统 A 本地事务执行完之后,发送个消息到 MQ;
    这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录
下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;
    要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝
试重新调用系统 B,反复 N 次,最后还是不行就放弃。
	最大努力通知方案实现方式比较简单,本质上就是通过定期校对,适用于数据一致性时间要求
不太高的场合,其实不把它看作是分布式事务方案,只认为是一种跨平台的数据处理方案也是
可以的。
    

小总结

    每一种解决方案都有各自的优缺点,不能说某一种方案绝对好,或者绝对差,只有在具体的情
景中,才能看出方案的优势与缺点,但是个人认为,如果订单插入之后要调用库存服务更新库存,
库存数据没有资金那么的敏感,可以用可靠消息最终一致性方案,一般的分布式事务场景,可以用
TCC方案。
	我只列举了集中比较常见的几种解决方案,希望可以给广大读者做一个参考,并在此基础上加
    以扩展。
    
     [🏆 技术专题第五期 | 聊聊分布式的那些事......]
     (https://juejin.cn/post/6872367966512644103)