1. 概览
在分布式系统中,系统间的通信除了大家所熟知的 RPC 外,基于 MQ 的异步通信也越来越流行,已经成为基础设施的重要组成部分。而 MQ 的引入对系统间的数据一致性提出了新的挑战,逐渐成为系统稳定性的一大隐患。
1.1. 背景
1.1.1. 业务挑战
未接触过分布式的同学可能对其没有概念,当我们引入 MQ 后,MQ 与数据库操作存在一致性要求。
举个简单例子,一个业务操作中存在 “更新DB” 和 “发送 MQ” 两个动作,具体如下:
image
如果流程正常结束,变更保存到 DB,Message 成功发送至 MQ,就不存在不一致的情况。但,如果中间发生异常,一致性就没有了保障。
比如在如下这个示例:
image
- 更新 DB 和 发送 MQ 被包在一个数据库事务;
- 如果在事务提交前,发送 MQ 之后出现了异常,将触发数据库事务回滚,此时
- DB 变更被回滚
- MQ 无法回滚
- 结果便是 Consumer 成功获取 Message 并进行业务处理,而 DB 回滚业务操作已经失败,下游处理了一个本不存在的变更。
那我们换个思路,数据库事务只对 DB 更新进行保护,示例如下:
image
- 仅将 数据库变更 包在一个数据库事务里;
- 如果在事务提交后,发送MQ 前出现了异常,此时
- 数据库变更已经成功持久化到 DB
- 而MQ发送失败,下游业务无法获取变更消息
- 最终导致丢失变更,未成功触发下游的正常业务;
当然还有更复杂的场景,示例如下:
image
数据库变更 和 发送MQ 交替出现,又该如何保障其一致性呢?
1.1.2. 事务消息
众所周知,RocketMQ 提供事务消息机制,以完成业务操作与消息发送的一致性。但在实际使用过程中,复杂的 API 将逻辑切分的稀碎,增加了业务理解的难度,在实际开发中很少使用。
事务消息整体流程如下:
image
核心流程如下:
- 生产者将半事务消息发送至 RocketMQ Broker。
- RocketMQ Broker 将消息持久化成功之后,向生产者返回 Ack 确认消息已经发送成功,此时消息暂不能投递,为半事务消息。
- 生产者开始执行本地事务逻辑。
- 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
- 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
- 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
- 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。
- 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 生产者根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。
为了确保一致性,整个流程变得好复杂,不仅仅是流程,API 使用也晦涩难懂,示例代码如下:
public class TransactionProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
// 通过监听器在本地事务中处理业务逻辑,对异常发现进行检测并恢复状态
TransactionListener transactionListener = new TransactionListenerImpl();
TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name");
// 为 Producer 设置监听器
producer.setTransactionListener(transactionListener);
producer.start();
String[] tags = new String[] {"TagA", &