分布式事务-MQ事务消息学习与落地方案
本文参考:
Apache RocketMQ 事务消息
基础概念
分布式事务解决的核心问题:降低分布式环境下如网络超时、系统宕机带来的不良影响,保证分布式系统中不同应用程序的数据一致性(重点关注事务 ACID 特性中的 A 原子性和 C 一致性,因为 I 隔离性在分布式环境中天然存在,持久化 D取决于业务是否需要将结果落库了)。
MQ 事务消息实现分布式事务「最终一致性」的核心:基于 half 消息 + commit/rollback 消息的两阶段提交,配合回调反查、MQ 重试机制,实现本地事务和下游消息发送的一致性。
看起来概念是有点复杂,想想看为啥别的方案就不行呢?主要还是有几个问题
- MQ 不可靠,可能会丢消息,而且我们会被 MQ 超时影响
- 发 MQ 消息和本地事务没法保证一致性
-
本地事务和发 MQ 消息放一块
本地事务发送成功了,机器宕机,MQ 发消息执行失败了,两者不一致 -
把 MQ 发送放到本地事务中
MQ 发送超时失败了,但有可能 MQ 是由于网络延迟,其实是发送到消费者且执行成功了。但本地事务捕获到异常直接回滚了,两者不一致
基本原理
拆解下来,主要有以下4个步骤:
- 发送半消息:上游向 RocketMQ Broker 发送一条半消息,该消息在 Broker 端的事务消息日志中被标记为 “prepared” 状态
- 执行本地事务:RocketMQ 会通知上游执行本地事务。如果本地事务执行成功,发送方通知 RocketMQ Broker 提交该事务消息
- 提交事务消息:RocketMQ 收到该提交消息以后,会讲该消息的状态从“prepared” 改为 “committed”,并使该消息可以被消费者消费
- 回滚事务消息:如果本地事务执行失败,应用程序通知 RocketMQ Broker 回滚该事务消息,RocketMQ 将该消息状态从 “prepared” 标记为 “rollback”,并将该消息从事务消息日志中删除,从而保证该消息不会被消费者消费。
在正常情况下,一切顺利,那么就是本地事务和发送消息都成功,下游也把消息消费成功。
我们看下非正常情况下的几种表现:
-
第一步半消息发送失败
不用执行本地事务,上游发送者直接会感知到发送失败,可以由上游决定是否需要重试
-
本地事务执行失败
直接发送 rollback 给 MQ broker,下游也收不到消息无需执行
-
第二个半消息发送失败
MQ 会在第 5 步发送回查消息,拿到上游回查的结果,第 7 步类似第 4 步,根据回查结果 commit/rollback 决定半消息是否提交或回滚 -
消息接收方消费失败
依赖 MQ 重投机制不断重试,也需要注意幂等实现
具体实现
首先 MQ 版本得支持事务消息,应该 RocketMQ 4.3.0正式引入了事务消息,上游发送者能感知到就行了。
假设上游业务是订单服务,下游业务是库存服务,我们想实现订单关单、库存回滚的分布式事务。我们重点是需要去实现 org.apache.rocketmq.client.producer.TransactionListener 这个接口,假设在 spring cloud 环境内:
- 在配置环境中,标明 producer 是事务的,并且 OrderCloseTransactionListener 实现了 TransactionListener 接口
- 实现 TransactionListener 的核心方法,executeLocalTransaction 是执行本地事务的方法,checkLocalTransaction 是用于 MQ 检查本地事务状态的方法
虽然上面 OrderCloseTransactionListener 看起来是通过 MQ 消费者的形式去配置的,但其实在发送者发送的时候,half 消息 + OrderCloseTransactionListener + commit/rollback 三步都是同步执行的。所以应该只是 spring cloud 接入写法的问题,这样子方便去拓展。
- 实现消费者去监听上述发送方消息
总结思考
这里主要是一些延伸思考,看下 MQ 事务消息的限制,以及它与其他分布式事务解决方案,比方说本地消息表、TCC 的对比和选型建议
使用限制
-
消息类型一致性
事务消息仅支持在 MessageType 为 Transaction 的主题内使用,即事务消息只能发送至类型为事务消息的主题中,发送的消息的类型必须和主题的类型一致。
-
消费事务性
事务消息仅保证上游本地事务和发送消息给下游两者的一致性,并不保证下游消费结果和上游事务的一致性。因此需要下游业务自行保障,有短暂的消费失败,也可以通过消息重试来达到最终处理成功,但需要做好幂等 -
中间状态可见性
MQ 事务消息为最终一致性,即在发送者消息提交到下游消费端处理完成前,下游分支和上游事务之间可能会存在不一致,因此事务消息仅适合接受异步执行短暂不一致的事务场景。需要业务去合理处理中间状态,有些中间态不需要对用户披露,后端不处理也行,有些会对用户表达,那就需要设计一下。
-
事务超时机制
事务消息的生命周期存在超时机制,即半事务消息被生产者发送服务端后,如果在指定时间内服务端无法确认提交或者回滚状态,则消息默认会被回滚。事务超时时间,可以参考 Apache RocketMQ 参数限制
选型思考建议
-
和本地消息表一样,都依赖 MQ 消息重投来保障最终一致性,非强一致性
-
实现成本上
MQ 事务消息首先需要 MQ 支持事务消息,需要实现 TransactionListener 接口,业务侵入性不低。
本地消息表不需要依赖 MQ 版本,对业务侵入性没那么高,但是也需要单独创建一张新表,由定时任务去轮询投递,改造成本也不低。 -
MQ 事务消息不像 TCC 一样,需要去关心业务回滚逻辑,回滚逻辑一般在长链路里都是蛮复杂的,并且 TCC 需要推动下游去提供接口,相比于让他们直接消费消息,TCC 接口的业务侵入性明显更高。
-
MQ 事务消息在消息可追踪性上可能不如本地消息表,它不会去额外存储消息。如果有一些消息持久化、对账的需求,可以使用本地消息表的方案,因为其会存储一遍待发送的消息,所以在消息回溯上会更好。