RocketMQ如何解决分布式事务(IT枫斗者)
那你说一说你是如何使用Rocket MQ解决你项目中分布式事务的呢?
一、RocketMQ顺序消息是什么?

二、RocketMQ的单向发送是什么?
这种方式主要用在不特别关心发送结果的场景,例如日志发送;

此方法返回前不会等待broker的确认,很显然,它具有最大的吞吐量,但有消息丢失的可能性;
通常消息的发送是这样一个过程:
客户端发送请求到服务器
服务器处理请求
服务器向客户端返回应答
所以,一次消息发送的耗时时间是上述三个步骤的总和,而某些场景要求耗时非常短,但是对可靠性要求并不高,例如日志收集类应用,此类应用可以采用oneway形式调用,oneway形式只发送请求不等待应答,而发送请求在客户端实现层面仅仅是一个操作系统系统调用的开销,即将数据写入客户端的socket缓冲区,此过程耗时通常在微秒级。
三、什么是RocketMQ事务消息?
Apache RocketMQ在4.3.0版中已经支持分布式事务消息,这里RocketMQ采用了2PC的思想来实现了提交事务消息,同时增加一个补偿逻辑来处理二阶段超时或者失败的消息,如下图所示:

上图说明了事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程;
1.事务消息发送及提交:
发送消息(half消息);
服务端响应消息写入结果;
根据发送结果执行本地事务(如果写入失败,此时half消息对业务不可见,本地逻辑不执行);
根据本地事务状态执行Commit或者Rollback(Commit操作生成消息索引,消息对消费者可见)。
2.补偿流程:
对没有Commit/Rollback的事务消息(pending状态的消息),从服务端发起一次“回查”;
Producer收到回查消息,检查回查消息对应的本地事务的状态;
根据本地事务状态,重新Commit或者Rollback;
其中,补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况。
事务消息一共有三种状态:提交状态、回滚状态、中间状态。
TransactionStatus.CommitTransaction: 提交事务,代表消费者可以消费此消息;
TransactionStatus.RollbackTransaction: 回滚事务,代表消息将被删除,不能被消费;
TransactionStatus.Unknown: 中间状态,代表需要检查消息队列来确定状态。
四、分布式事务的解决
事务是数据库的概念,数据库事务(ACID);分布式事务的产生,是由于数据库的拆分和分布式架构(微服务)带来的,在常规情况下,我们在一个进程中操作一个数据库,这属于本地事务,如果在一个进程中操作多个数据库,或者在多个进程中操作一个或多个数据库,就产生了分布式事务;
(1)数据库分库分表就产生了分布式事务;

(2)项目拆分服务化也产生了分布式事务;

1、X/Open DTP分布式事务处理参考模型
全称X/Open Distributed Transaction Processing Reference Model,即分布式事务处理参考模型;X/Open是一个组织机构,它定义出的一套分布式事务处理的标准和规范,具体实现由不同厂商来实现;
Java EE 遵循了X/open DTP规范,设计并实现了Java里面的分布式事务编程接口规范JTA;XA是X/Open DTP定义的中间件与数据库之间的接口规范,主流的数据库厂商都实现了XA接口函数规范。
2、分布式事务的实现主要有以下 5 种方案:
(1)XA方案 (一般数据库实现了)
(2)TCC方案
(3)本地消息异步确认(不是标准也不是规范,是实践中总结出来的)
(4)可靠消息最终一致性(不是标准也不是规范,是实践中总结出来的)
(5)最大努力通知(不是标准也不是规范,是实践中总结出来的)
1.XA方案

2.TCC 补偿方案

3.本地消息异步确保
本地消息异步确保是 ebay 提出来的一套分布式事务解决方案;
具体实现方案是:


4.可靠消息最终一致性方案(国内互联网)
该方案是本地消息异步确保方案的升级和优化,该方案中去掉了本地消息表;

该方案是ebay改进版,可以适用高并发场景;
5.最大努力通知方案(也是保证最终一致性)
该方案的具体实现是:
系统 A 本地事务执行完之后,发送个消息到 MQ;
同时有一个专门消费 MQ 最大努力通知服务,该服务会消费 MQ 消息然后写入数据库中,或者是放入缓存中,然后调用系统 B 的接口;
要是系统 B 执行成功就执行完毕,如果系统 B 执行失败,那么最大努力通知服务将定时尝试重新调用系统 B,反复多次,尝试多次后如果依然失败则放弃;(这时候可以监控、业务后台处理,人工干预等)

比如在支付业务,通知支付结果通常采用该方案;
另外可以根据不同的业务定制不同的通知规则,比如通知支付结果等相对严谨的业务,可以将通知频率设置高一些,通知时间长一些,比如隔 5 分钟通知一次,持续时间 1 小时。
如果不重要的业务,比如通知用户增加积分,则可以将通知频率设置低一些,时间短一些,比如 10 分钟通知一次,持续 30 分钟。
分布式事务总结
互联网的分布式事务解决方案
目前互联网领域里的几种流行的分布式解决方案,都没有像之前所说的XA事务一样形成X/OpenDTP那样的工业规范,而是仅仅在具体的行业里获得较多的认可。
1、业务接口整合,避免分布式事务
这个就是把一个业务流程中需要在一个事务里执行的多个相关业务接口包装整合到一个事务中,比如我们可以将A/B/C整合为一个服务D来实现单一事务业务服务。
2、ebay 模式或升级版
3、最大努力通知方案
4、开源中间件方案
TCC强一致性:刚性事务
最终一致性:柔性事务
五、RocketMQ解决分布式事务
分布式事务是一个复杂的技术问题,没有哪个可以通用的方案,如果我们的系统不追求强一致性,那么最常用的分布式事务解决方案就是最终一致性方案,RocketMQ提供的事务消息特性可以实现消息最终一致性的分布式事务解决方案。
1、RocketMQ的事务消息:

(1)Half Message,半消息
暂时不能被Consumer消费的消息,Producer已经把消息发送到Broker端,但是此消息的状态被标记为不能投递,处于这种状态下的消息称为半消息,事实上,该状态下的消息会被放在一个叫做 RMQ_SYS_TRANS_HALF_TOPIC的主题下。
当 Producer端对它二次确认后,也就是Commit之后, Consumer端才可以消费到,那么如果是Rollback,该消息则会被删除,永远不会被消费到。
(2)事务状态回查
可能因为网络原因、应用问题等导致Producer端一直没有对这个半消息进行确认,那么此时Broker服务器会定时扫描这些半消息,主动回调Producer端查询该消息的状态。
总之RocketMQ事务消息的实现原理就是基于两阶段提交和事务状态回查,来决定消息最终是提交还是回滚。
2、实例:减库存、下订单业务场景

1、常规操作
场景一:先减库存后发消息
先减库存再发消息,万一发送消息失败了,那用户就没法下单;
场景二:先发消息,后减库存
消息发送成功,但用户减库存失败,下单业务订阅到了消息,导致用户下了单;
总之都会出现两边数据不一致的问题,怎么解决?
2、RocketMQ把消息分为两个阶段:预备阶段和确认阶段;
Prepared阶段(预备阶段)
该阶段主要发一个消息到rocketmq,但该消息只储存在commitlog中,但consumeQueue中不可见,也就是消费端(订阅端)无法看到此消息;
commit/rollback阶段(确认阶段)
该阶段主要是把prepared消息保存到consumeQueue中,即让消费端可以看到此消息,也就是可以消费此消息。
3、回到减库存下订单案例中:

4、异常情况分析:

5、回查判断业务是否成功的设计
设计一张TransactionLog表,将业务表的处理和TransactionLog表的处理放在同一个本地事务中,如果本地减库存事务成功,TransactionLog表中应该也成功记录了TransactionId,当RocketMQ回查时,只需检查对应TransactionId是否存在即可。
6、消费端的设计
消费端(下订单业务)需要考虑幂等性设计,消费消息失败要重试几次,幂等性设计一般可以采用数据库唯一约束或者分布式锁解决,消费重试RocketMQ本身就支持,默认重试16次,可以修改。