一、背景:为什么要引入事务消息
在高并发下单、支付、状态变更等核心业务中,我们通常会采用如下流程:
1. 本地事务(订单落库)2. 发送 MQ 消息(通知下游系统)
但这个流程存在一个致命问题:
数据库事务与消息发送并不具备原子性
一旦出现:
-
数据已提交
-
消息发送失败
系统就会进入数据不一致状态。
为了解决这个问题,RocketMQ 提供了 事务消息机制,用于保证:
本地事务执行结果 与 消息投递结果的最终一致性
二、事务消息的核心概念:Half Message(半消息)
RocketMQ 事务消息并不是"先执行事务,再发消息",而是采用了一个中间态设计:
Half Message(也称 Prepare Message)
半消息的特点:
-
已经被 Broker 持久化
-
对消费者 不可见
-
等待 Producer 给出"事务裁决"
这是整个事务消息机制的基础。
三、从代码入口看完整流程
在 Spring Boot 中,事务消息的入口通常是:
java
rocketMQTemplate.sendMessageInTransaction( "order-created-topic", message, businessArg );
这一行代码背后,隐藏的是一整套 严格定义的时序流程。
四、事务消息的完整时序拆解(关键)
阶段一:Producer 发送半消息
Producer 首先向 Broker 发送一条 Prepare Message:
-
Broker 接收到后:
-
将消息持久化
-
标记为
PREPARED -
不会投递给任何消费者
-
此时 Broker 会返回一个 ACK。
注意:
这个 ACK 仅代表"半消息存储成功",不代表事务成功
阶段二:ACK 返回后,事务监听器被触发
这是很多开发者最容易误解的地方。
很多人以为:
收到 ACK → 再回调 → 再触发事务监听器
但真实情况是:
RocketMQ 客户端在收到半消息 ACK 后,会立即在同一调用链路中触发本地事务执行
也就是说:
-
ACK 不会"传递"给你
-
你也无需监听 ACK
-
一切由 RocketMQ 客户端内部状态机控制
五、事务监听器的真实角色
在 Spring Boot 中,事务监听器通常实现:
java
public class OrderTransactionListener
implements RocketMQLocalTransactionListener
executeLocalTransaction:事务裁决入口
java
@Override
public RocketMQLocalTransactionState executeLocalTransaction(
Message msg, Object arg) {
// 执行本地事务
}
这个方法的调用时机是:
半消息发送成功(ACK 返回)之后,立即调用
此时你要做的事情只有一件:
执行本地事务,并给出事务裁决结果
事务监听器返回值的真正含义
java
return RocketMQLocalTransactionState.COMMIT;
这行代码的本质含义是:
Producer 明确告诉 Broker:
本地事务已经成功,可以提交这条消息
RocketMQ 提供了三种裁决结果:
| 返回值 | 含义 |
|---|---|
| COMMIT | 本地事务成功,消息可投递 |
| ROLLBACK | 本地事务失败,消息丢弃 |
| UNKNOWN | 状态不确定,等待回查 |
这里不是"接收 ACK",而是"向 Broker 给最终裁决"
六、Broker 如何感知事务结果
COMMIT
-
Broker 将半消息转为普通消息
-
消费者开始可见
ROLLBACK
-
Broker 删除半消息
-
消息彻底结束生命周期
UNKNOWN
-
Broker 将该消息标记为"待确认"
-
稍后主动发起 事务回查
七、事务回查机制(第二道保险)
当出现以下情况之一时:
-
Producer 返回
UNKNOWN -
Producer 在执行本地事务时宕机
-
Broker 长时间未收到事务裁决
Broker 会主动回查 Producer。
回查入口方法
java
@Override
public RocketMQLocalTransactionState checkLocalTransaction(
Message msg)
回查的本质
Broker 并不是相信 Producer 的返回值
而是要求 Producer 基于真实数据状态重新裁决
典型做法是:
-
查询数据库中订单是否存在
-
根据结果返回 COMMIT 或 ROLLBACK
八、事务监听器如何与 Producer 绑定
主要是通过RocketMQTemplate进行绑定的,所以在多template场景下才需要进行绑定
java
@RocketMQTransactionListener(
rocketMQTemplateBeanName = "orderRocketMQTemplate"
)
九、一个容易踩坑但非常重要的设计原则
在事务监听器中:
不建议直接注入 Repository
强烈建议通过 Service 层执行业务逻辑
原因在于:
-
事务监听器由 RocketMQ 客户端线程触发
-
本地事务、代理、AOP 需要 Spring 正确管理
-
Service 层是事务边界的最佳落点
十、用一句话总结 RocketMQ 事务消息的本质
RocketMQ 事务消息并非通过 ACK 回调驱动业务逻辑,而是通过"半消息 + 本地事务裁决 + 事务回查"机制,将消息提交的最终决定权交由 Producer,从而实现消息投递与本地事务的最终一致性。