原文来自:(https://www.cnblogs.com/sujing/p/11006424.html)
分布式事务就是在分布式的场景下,需要满足事务的需求!上篇文章我们聊过了消息中间件,那这篇文章我们要聊的是分布式事务,把两者一结合,便有了基于消息中间件的分布式事务解决方案!不管是本地事务,还是分布式事务,都是为了解决数据的一致性问题!一致性这个词咱们前面多次提及!与本地事务不同的是,分布式事务需要保证的是分布式环境下,不同数据库表中的数据的一致性问题。分布式事务的解决方案有多种,如XA协议、TCC三阶段提交、基于消息队列等等,本文只会涉及基于消息队列的解决方案!
本地事务讲到了一致性,分布式事务不可避免的面临着一致性的问题!回到最开始跨行转账的例子,如果A银行用户向B银行用户转账,正常流程应该是:
1、A银行对转出账户执行检查校验,进行金额扣减。
2、A银行同步调用B银行转账接口。
3、B银行对转入账户进行检查校验,进行金额增加。
4、B银行返回处理结果给A银行。
在正常情况对一致性要求不高的场景,这样的设计是可以满足需求的。但是像银行这样的系统,如果这样实现大概早就破产了吧。我们先看看这样的设计最主要的问题:
1、同步调用远程接口,如果接口比较耗时,会导致主线程阻塞时间较长。
2、流量不能很好控制,A银行系统的流量高峰可能压垮B银行系统(当然B银行肯定会有自己的限流机制)。
3、如果“第1步”刚执行完,系统由于某种原因宕机了,那会导致A银行账户扣款了,但是B银行没有收到接口的调用,这就出现了两个系统数据的不一致。
4、如果在执行“第3步”后,B银行由于某种原因宕机了而无法正确回应请求(实际上转账操作在B银行系统已经执行且入库),这时候A银行等待接口响应会异常,误以为转账失败而回滚“第1步”操作,这也会出现了两个系统数据的不一致。
对于问题的1、2都很好解决,如果对消息队列熟悉的朋友应该很快能想到可以引入消息中间件进行异步和削峰处理,于是又重新设计了一个方案,流程如下:
1、A银行对账户进行检查校验,进行金额扣减。
2、将对B银行的请求异步写入队列,主线程返回。
3、启动后台程序从队列获取待处理数据。
4、后台程序对B银行接口进行远程调用。
5、B银行对转入账户进行检查校验,进行金额增加。
6、B银行处理完成回调A银行接口通知处理结果。
通过上面的图我们能看到,引入消息队列后,系统的复杂性瞬间提升了,虽然弥补了我们第一种方案的几个不足点,但也带来了更多的问题,比如消息队列系统本身的可用性、消息队列的延迟等等!并且,这样的设计依然没有解决我们面临的核心问题-数据的一致性!
1、如果“第1步”刚执行完,系统由于某种原因宕机了,那会导致A银行账户扣款了,但是写入消息队列失败,无法进行B银行接口调用,从而导致数据不一致。
2、如果B银行在执行“第5步”时由于校验失败而未能成功转账,在回调A银行接口通知回滚时网络异常或者宕机,会导致A银行转账无法完成回滚,从而导致数据不一致。
面对上述问题,我们不得不对系统再次进行升级改造。为了解决“A银行账户扣款了,但是写入消息队列失败”的问题,我们需要借助一个转账日志表,或者叫转账流水表,该表简单的设计如下:
字段名称 | 字段描述 |
---|---|
tId | 交易流水id |
accountNo | 转出账户卡号 |
targetBankNo | 目标银行编码 |
targetAccountNo | 目标银行卡号 |
amount | 交易金额 |
status | 交易状态(待处理、处理成功、处理失败) |
lastUpdateTime | 最后更新时间 |
这个流水表需要怎么用呢?我们在“第1步”进行扣款时,同时往流水表写入一条操作流水,状态为“待处理”,并且这两个操作必须是原子的,也就是说必须通过本地事务保证这两个操作要么同时成功,要么同时失败!这就保证了只要转账扣款成功,必定会记录一条状态为“待处理”的转账流水。如果在这一步失败了,那自然就是转账失败,没有后续操作了。如果这步操作后系统宕机了导致没有将消息成功写入消息队列(也就是“第2步”)也没关系,因为我们的流水数据已经持久化了!这时候我们只需要加入一个后台线程进行补偿,定期的从转账流水表中读取状态为“待处理”且最后更新的时间距当前时间大于某个阈值的数据,重新放入消息队列进行补偿。这样,就保证了消息即使丢失,也会有补偿机制!B银行在处理完转账请求后会回调A银行的接口通知转账的状态,从而更新A银行流水表中的状态字段!这样就完美解决了上一个方案中的两个不足点。系统设计图如下:
到目前为止,我们很好的解决了消息丢失的问题,保证了只要A银行转账操作成功,转账的请求就一定能发送到B银行!但是该方案又引入了一个问题,通过后台线程轮询将消息放入消息队列处理,同一次转账请求可能会出现多次放入消息队列而多次消费的情况,这样B银行会对同一转账多次处理导致数据出现不一致!那怎么保证B银行转账接口的幂等性呢?
同样的,我们可以在B银行系统中需要增加一个转账日志表,或者叫转账流水表,B银行每次接收到转账请求,在对账户进行操作的时候同时往转账日志表中插入一条转账日志记录,同样这两个操作也必须是原子的!在接收到转账请求后,首先根据唯一转账流水Id在日志表中查找判断该转账是否已经处理过,如果未处理过则进行处理,否则直接回调返回! 最终的架构图如下:
所以,我们这里最核心的就是A银行通过本地事务保证日志记录+后台线程轮询保证消息不丢失。B银行通过本地事务保证日志记录从而保证消息不重复消费!B银行在回调A银行的接口时会通知处理结果,如果转账失败,A银行会根据处理结果进行回滚。
当然,分布式事务最好的解决方案是尽量避免出现分布式事务!