)
在之前的文章里我们把 RocketMQ 的存储、发送、消费全链路都拆解了一遍。但有一个重量级特性我们一直没聊——事务消息Transactional Message。为什么把事务消息放在这里才讲因为理解事务消息需要两个前提一是你要知道消息是怎么存储的CommitLog ConsumeQueue二是你要知道消息是怎么发送的同步/异步/单向。有了这些基础我们才能深入理解事务消息的精妙之处。说实话事务消息是 RocketMQ 最复杂、也最令人惊叹的特性之一。它解决的是一道难题——在分布式系统中如何保证“发送消息”和“执行本地事务”要么一起成功要么一起失败今天这篇文章我们就来彻底搞懂事务消息。我会从它要解决的问题开始一步步深入到半消息、事务回查、状态机等核心机制。老规矩配合流程图一步一图。八、事务消息事务消息诞生的背景与要解决的问题我们先来看一个经典的分布式事务困境。你在电商系统里做一个“下单扣库存”的业务。订单服务和库存服务是两个独立的服务通过消息来解耦分布式事务困境订单服务创建订单发送扣库存消息消息发送成功但订单插入失败⚠️ 消息已经发出去了库存被扣了但订单不存在→ 数据不一致订单插入成功但消息发送失败⚠️ 订单创建了但库存没扣→ 超卖风险问题可以归结为一句话本地数据库操作订单插入和消息发送扣库存通知这两个操作不在同一个事务里。它们要么都成功要么都失败——但在分布式环境下这很难做到。常见的解决方案及其局限方案 怎么做 问题先发消息再执行本地事务 消息发出去后再插入订单 消息发成功但订单插入失败 → 库存被误扣先执行本地事务再发消息 插入订单后再发消息 订单插入成功但消息发送失败 → 超卖本地事务 本地消息表 在同一个数据库里存消息表定时扫描发送 依赖数据库轮询效率低有延迟RocketMQ 事务消息的解法RocketMQ 事务消息的核心思想是让消息的发送和本地事务的执行通过“半消息 事务回查”机制达成最终一致性。它不依赖本地消息表不需要轮询不需要额外的存储全部在 Broker 和 Producer 之间协调完成。事务消息的核心流程半消息Half Message什么是半消息Half Message半消息是事务消息的核心概念。它是一种暂不可见的消息——Producer 发送了这条消息但 Consumer 暂时还消费不到。半消息✅ 消息已写入 CommitLog有存储❌ 消息在 ConsumeQueue 中被标记为不可见⏳ 等待本地事务执行结果半消息 消息已存储但不可消费像个“待确认”的订单半消息的设计意图半消息的存在解决了“消息发送成功了但本地事务还没执行”这个中间状态的问题。如果半消息直接可见Consumer 就能消费到——但此时本地事务可能还没执行完数据状态不确定如果半消息不可见Consumer 就消费不到——等本地事务执行完再决定是提交还是回滚这样消息的可见性和本地事务的执行结果绑定在了一起。本地事务与半消息的协调机制事务消息的完整流程核心就是一个两阶段协调的过程成功失败COMMITROLLBACK业务发起事务消息阶段一发送半消息Producer 发送半消息到 BrokerBroker 存储半消息标记为不可消费Broker 返回半消息发送成功阶段二执行本地事务Producer 执行本地业务逻辑如插入订单本地事务结果阶段三提交/回滚Producer 向 Broker发送事务状态状态Broker 将半消息标记为可见Consumer 可以消费到消息Broker 删除半消息标记删除消息被丢弃不消费关键点半消息是第一步先发半消息再执行本地事务。如果半消息发送失败直接结束不用执行本地事务。本地事务决定最终状态本地事务成功 → COMMIT本地事务失败 → ROLLBACK。Broker 最终执行COMMIT 使消息可见ROLLBACK 使消息被丢弃。事务回查Check机制的实现原理上面我们讨论的是正常情况——本地事务执行完Producer 能正常告诉 Broker 是 COMMIT 还是 ROLLBACK。但如果本地事务执行完后Producer 宕机了或者网络断了呢Broker 怎么知道这条半消息最终应该被提交还是回滚答案就是事务回查Transaction Check机制。否是否是否是Broker 扫描半消息半消息状态是否为 UNKNOWN跳过Broker 向 Producer发起事务回查请求Producer 是否能正常响应标记为需要再次回查等待下一轮回查次数是否超限默认回滚防止消息堆积Producer 执行事务状态检查逻辑返回事务状态COMMIT / ROLLBACK / UNKNOWNBroker 根据状态执行提交或回滚回查机制的核心设计Broker 主动发起Broker 会定期扫描处于 UNKNOWN 状态的半消息默认每 60 秒一次Producer 被动响应Producer 需要实现 TransactionListener 的 checkLocalTransaction() 方法根据业务状态返回最终决定可配置重试回查失败会重试但如果超过配置的重试次数默认 15 次Broker 会默认回滚为什么需要回查回查解决了分布式系统中的不确定性问题——当 Producer 不可达时Broker 不能无限等待必须有最终决策。回查机制确保了半消息不会永远处于“悬而未决”的状态。事务消息的状态COMMIT、ROLLBACK、UNKNOWNRocketMQ 事务消息有三种状态由 Producer 的 TransactionListener 返回状态 含义 Broker 操作 Consumer 是否可见COMMIT 本地事务成功提交消息 将半消息标记为可见 ✅ 可见可消费ROLLBACK 本地事务失败回滚消息 删除半消息 ❌ 不可见被丢弃UNKNOWN 本地事务状态未知 暂不操作等待回查 ❌ 暂不可见等待决策public class OrderTransactionListener implements TransactionListener {Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { try { // 执行本地事务插入订单 orderService.createOrder(arg); return LocalTransactionState.COMMIT_MESSAGE; } catch (Exception e) { return LocalTransactionState.ROLLBACK_MESSAGE; } } Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { // 回查时根据业务数据判断事务是否成功 String orderId msg.getProperty(orderId); if (orderService.orderExists(orderId)) { return LocalTransactionState.COMMIT_MESSAGE; } return LocalTransactionState.ROLLBACK_MESSAGE; }}UNKNOWN 的使用场景本地事务执行过程中发生了异常无法确定是否成功本地事务执行时间很长需要异步确认依赖外部系统的状态需要等待外部回调UNKNOWN 状态触发的回查机制给了系统足够的容错空间。事务消息的完整时序图与执行流程把前面所有的内容串起来我们来看一张完整的时序图本地数据库ConsumerBrokerProducer业务应用本地数据库ConsumerBrokerProducer业务应用消息不可见不被消费此时不向 Broker 发送最终决定alt[多次回查仍返回UNKNOWN]loop[事务回查默认每 60 秒]alt[本地事务成功][本地事务失败][本地事务执行异常/超时]发送事务消息发送半消息状态PREPARED存储半消息不可消费记录事务状态半消息发送成功回调执行本地事务执行本地事务插入订单等7a. 事务提交成功8a. 返回 COMMIT9a. 提交事务消息10a. 标记消息可见11a. Consumer 消费到消息7b. 事务回滚8b. 返回 ROLLBACK9b. 回滚事务消息10b. 删除半消息8c. 返回 UNKNOWN9c. 发起回查请求10c. 检查本地事务状态11c. 查询业务数据12c. 返回查询结果13c. 返回 COMMIT/ROLLBACK/UNKNOWN14c. 提交最终事务状态15c. 超过回查次数限制默认回滚流程概括发送半消息 → 2. 执行本地事务 → 3. 根据结果决定状态 → 4. COMMIT/ROLLBACK/UNKNOWN → 5. UNKNOWN 触发回查 → 6. 最终确定状态。这个流程保证了要么本地事务成功且消息被消费要么本地事务失败且消息被丢弃。