RocketMQ 事务消息是解决分布式事务的核心方案,基于「两阶段提交 + 消息回查」机制,保证「本地事务」与「消息发送」的最终一致性。
一、事务消息核心原理(先理解再上手)
1. 分布式事务痛点
在跨服务/跨库场景中(如「下单扣库存」),传统方式易出现:
- 本地事务提交成功,但消息发送失败 → 下游服务未执行;
- 消息发送成功,但本地事务回滚 → 下游服务执行了无效操作。
2. RocketMQ 事务消息流程(两阶段+回查)
RocketMQ 事务消息通过「半消息(Half Message)+ 本地事务 + 提交/回滚 + 回查」解决一致性问题,核心流程:
消费者 回查服务 生产者 回查服务(CheckListener) 消费者(Consumer) RocketMQ Broker 生产者(Producer) 消费者 回查服务 生产者 回查服务(CheckListener) 消费者(Consumer) RocketMQ Broker 生产者(Producer) alt [本地事务成功] [本地事务失败] [本地事务结果未知(超时/异常)] 1. 发送半消息(暂不投递) 半消息发送成功 2. 执行本地事务(如订单入库) 3. 提交消息(Broker标记为可投递) 3. 回滚消息(Broker删除半消息) 4. 定时回查本地事务状态 5. 返回事务结果(提交/回滚) 6. 仅提交的消息会被投递到消费者 7. 消费确认(ACK)
3. 核心概念
| 术语 | 作用 |
|---|---|
| 半消息 | 发送到 Broker 但暂不投递的消息,仅生产者可确认其最终状态(提交/回滚) |
| 本地事务 | 生产者端的数据库操作(如订单入库、库存扣减) |
| 事务监听器 | 生产者端实现,包含「执行本地事务」和「回查本地事务状态」两个核心方法 |
| 消息回查 | Broker 对超时未确认的半消息,主动查询生产者本地事务状态 |
二、完整代码实现(Spring Boot + RocketMQ 事务消息)
前置准备
- 安装 RocketMQ 并启动 NameServer、Broker(需开启事务消息支持,默认开启);
- Spring Boot 项目引入依赖:
xml
<!-- RocketMQ Spring Boot Starter(适配 4.x/5.x) -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
- 配置
application.yml:
yaml
rocketmq:
name-server: 127.0.0.1:9876 # NameServer 地址
producer:
group: order-transaction-group # 事务生产者组(必须指定)
send-message-timeout: 30000 # 发送超时时间
retry-times-when-send-failed: 2 # 发送失败重试次数
步骤1:定义事务消息实体
java
import lombok.Data;
/**
* 订单事务消息实体
*/
@Data
public class OrderTransactionMessage {
private Long orderId; // 订单ID
private Long productId; // 商品ID
private Integer num; // 购买数量
private String orderNo; // 订单编号
}
步骤2:实现事务监听器(核心)
事务监听器是生产者端的核心,需实现 RocketMQLocalTransactionListener,包含「执行本地事务」和「回查事务状态」两个方法:
java
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
/**
* 订单事务消息监听器
* @RocketMQTransactionListener:标记为事务监听器,绑定生产者组
*/
@Component
@RocketMQTransactionListener(txProducerGroup = "order-transaction-group")
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
/**
* 第一步:执行本地事务(半消息发送成功后触发)
* @param msg 半消息内容
* @param arg 自定义参数(发送消息时传入)
* @return 事务状态:COMMIT/ROLLBACK/UNKNOWN
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
// 1. 解析消息(获取订单数据)
OrderTransactionMessage orderMsg = (OrderTransactionMessage) msg.getPayload();
System.out.println("【执行本地事务】开始处理订单:orderId=" + orderMsg.getOrderId());
// 2. 执行本地事务核心逻辑(如订单入库、库存预扣减)
// 真实场景:调用订单Service保存订单,调用库存Service预扣减库存
boolean isSuccess = saveOrder(orderMsg); // 模拟订单入库
// 3. 根据本地事务结果返回状态
if (isSuccess) {
System.out.println("【本地事务成功】订单入库完成,提交消息");
return RocketMQLocalTransactionState.COMMIT; // 提交消息(Broker投递)
} else {
System.out.println("【本地事务失败】订单入库失败,回滚消息");
return RocketMQLocalTransactionState.ROLLBACK; // 回滚消息(Broker删除)
}
} catch (Exception e) {
System.out.println("【本地事务异常】未知状态,等待Broker回查:" + e.getMessage());
// 异常时返回UNKNOWN,Broker会定时回查事务状态
return RocketMQLocalTransactionState.UNKNOWN;
}
}
/**
* 第二步:事务回查(Broker对UNKNOWN状态的消息触发)
* @param msg 半消息内容
* @return 事务状态:COMMIT/ROLLBACK/UNKNOWN(建议仅返回前两者)
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 1. 解析消息
OrderTransactionMessage orderMsg = (OrderTransactionMessage) msg.getPayload();
System.out.println("【事务回查】检查订单状态:orderId=" + orderMsg.getOrderId());
// 2. 查数据库/缓存,确认本地事务最终状态
// 真实场景:查询订单表,判断订单是否已入库
boolean isOrderSuccess = checkOrderStatus(orderMsg.getOrderId());
// 3. 返回最终状态(Broker根据此状态决定提交/回滚)
if (isOrderSuccess) {
System.out.println("【回查结果】订单已入库,提交消息");
return RocketMQLocalTransactionState.COMMIT;
} else {
System.out.println("【回查结果】订单未入库,回滚消息");
return RocketMQLocalTransactionState.ROLLBACK;
}
}
// ------------------- 模拟业务方法 -------------------
/**
* 模拟订单入库
*/
private boolean saveOrder(OrderTransactionMessage msg) {
// 真实场景:执行insert into t_order ...
// 模拟成功:return true;模拟失败:return false;模拟异常:throw new RuntimeException()
return true;
}
/**
* 模拟查询订单状态(回查时调用)
*/
private boolean checkOrderStatus(Long orderId) {
// 真实场景:select * from t_order where order_id = ?
return true;
}
}
步骤3:发送事务消息(生产者)
创建生产者 Service,调用 RocketMQTemplate 的 sendMessageInTransaction 方法发送事务消息:
java
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
/**
* 订单事务消息生产者
*/
@Service
public class OrderTransactionProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 发送订单事务消息
* @param orderMsg 订单消息
*/
public void sendOrderTransactionMessage(OrderTransactionMessage orderMsg) {
// 1. 构建消息(topic:order_topic,tag:transaction)
org.springframework.messaging.Message<OrderTransactionMessage> message = MessageBuilder
.withPayload(orderMsg)
.build();
// 2. 发送事务消息
// 参数:topic:tag、消息体、自定义参数(传递给本地事务方法)
rocketMQTemplate.sendMessageInTransaction(
"order_topic:transaction",
message,
null // 自定义参数,可传递订单相关数据
);
System.out.println("【事务消息发送】半消息已发送:orderId=" + orderMsg.getOrderId());
}
}
步骤4:消费事务消息(消费者)
消费者仅能收到「提交状态」的事务消息,实现普通消费逻辑即可:
java
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
/**
* 订单事务消息消费者(仅消费提交的消息)
*/
@Component
@RocketMQMessageListener(
topic = "order_topic",
consumerGroup = "order-consumer-group",
selectorExpression = "transaction" // 匹配tag
)
public class OrderTransactionConsumer implements RocketMQListener<OrderTransactionMessage> {
@Override
public void onMessage(OrderTransactionMessage msg) {
// 消费逻辑:如扣减实际库存、发送短信通知、生成物流单等
System.out.println("【消费事务消息】开始处理:orderId=" + msg.getOrderId());
// 真实场景:stockService.deductStock(msg.getProductId(), msg.getNum());
// smsService.sendNotify(msg.getOrderNo());
System.out.println("【消费完成】订单下游业务处理完毕:orderNo=" + msg.getOrderNo());
}
}
步骤5:测试验证
创建测试控制器,模拟订单创建并发送事务消息:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderTestController {
@Autowired
private OrderTransactionProducer producer;
/**
* 测试发送事务消息
* 访问:http://localhost:8080/createOrder/1001/1/2
*/
@GetMapping("/createOrder/{orderId}/{productId}/{num}")
public String createOrder(
@PathVariable Long orderId,
@PathVariable Long productId,
@PathVariable Integer num) {
// 构建订单消息
OrderTransactionMessage msg = new OrderTransactionMessage();
msg.setOrderId(orderId);
msg.setProductId(productId);
msg.setNum(num);
msg.setOrderNo("ORDER_" + System.currentTimeMillis());
// 发送事务消息
producer.sendOrderTransactionMessage(msg);
return "事务消息发送成功,订单号:" + msg.getOrderNo();
}
}
测试结果(本地事务成功)
控制台输出:
【事务消息发送】半消息已发送:orderId=1001
【执行本地事务】开始处理订单:orderId=1001
【本地事务成功】订单入库完成,提交消息
【消费事务消息】开始处理:orderId=1001
【消费完成】订单下游业务处理完毕:orderNo=ORDER_1710999999999
测试结果(本地事务异常)
修改 saveOrder 方法抛出异常,输出:
【事务消息发送】半消息已发送:orderId=1002
【执行本地事务】开始处理订单:orderId=1002
【本地事务异常】未知状态,等待Broker回查:模拟异常
(Broker 定时回查)
【事务回查】检查订单状态:orderId=1002
【回查结果】订单未入库,回滚消息
(消费者无消费日志,消息被Broker删除)
三、RocketMQ 事务消息核心使用场景
场景1:电商下单(订单+库存+物流)
- 痛点:订单入库、库存扣减、物流创建跨服务,需保证一致性;
- 方案 :
- 生产者发送「订单创建」半消息;
- 本地事务执行「订单入库 + 库存预扣减」;
- 提交消息后,消费者执行「实际扣减库存 + 创建物流单」;
- 若本地事务失败,回滚消息,避免库存无效扣减。
场景2:支付回调(支付结果+订单状态更新)
- 痛点:第三方支付回调异步通知,需保证「支付成功」与「订单改已支付」一致;
- 方案 :
- 支付回调服务发送「支付结果」半消息;
- 本地事务更新订单状态为「已支付」;
- 提交消息后,消费者执行「发放优惠券 + 积分增加」;
- 若更新订单失败,回滚消息,支付平台可重试回调。
场景3:金融转账(转出+转入)
- 痛点:A账户扣款、B账户加款需原子性,跨库/跨服务无法用本地事务;
- 方案 :
- 转账服务发送「转账请求」半消息;
- 本地事务执行「A账户扣款」;
- 提交消息后,消费者执行「B账户加款」;
- 若A账户扣款失败,回滚消息;若扣款成功但B加款失败,通过消息重试保证最终一致性。
场景4:数据同步(业务库+ES/缓存)
- 痛点:业务数据入库后,需同步到ES/Redis,避免「入库成功、同步失败」;
- 方案 :
- 业务服务发送「数据变更」半消息;
- 本地事务执行「数据入库」;
- 提交消息后,消费者执行「ES/Redis同步」;
- 同步失败可通过消息重试机制补全。
场景5:跨系统通知(订单+短信/邮件)
- 痛点:订单创建后需发送短信/邮件,避免「订单创建失败但短信已发送」;
- 方案 :
- 订单服务发送「订单创建」半消息;
- 本地事务执行「订单入库」;
- 提交消息后,消费者执行「发送短信/邮件」;
- 订单入库失败则回滚消息,避免无效通知。
四、避坑要点(高频问题)
1. 事务监听器不生效
- 原因:
@RocketMQTransactionListener的txProducerGroup与生产者组不一致; - 解决:保证监听器的
txProducerGroup=application.yml中producer.group。
2. 回查方法被频繁调用
- 原因:本地事务返回
UNKNOWN且回查方法多次返回UNKNOWN; - 解决:
- 回查方法必须返回
COMMIT/ROLLBACK,避免返回UNKNOWN; - 优化回查逻辑,确保能快速获取事务最终状态;
- 调整 Broker 回查参数(
transactionTimeout超时时间、transactionCheckMax最大回查次数)。
- 回查方法必须返回
3. 消息重复消费
- 原因:Broker 重试投递、消费者ACK失败;
- 解决:
- 消费者实现幂等消费(如根据订单ID判断是否已处理);
- 消息携带唯一ID,消费前查缓存/数据库是否已处理。
4. 本地事务与消息提交不一致
- 原因:本地事务提交成功,但提交消息时网络异常;
- 解决:依赖 Broker 回查机制,回查方法确认本地事务成功后提交消息。
5. 事务消息性能问题
- 原因:半消息存储、回查机制增加开销;
- 优化:
- 批量发送事务消息(非高频场景);
- 缩短本地事务执行时间(避免长事务);
- 调整回查频率,避免高频回查。
五、核心总结
- 核心机制:RocketMQ 事务消息通过「半消息 + 本地事务 + 提交/回滚 + 回查」保证分布式事务最终一致性;
- 实现关键 :
- 生产者:调用
sendMessageInTransaction发送半消息; - 监听器:实现
executeLocalTransaction(执行本地事务)和checkLocalTransaction(回查); - 消费者:仅消费提交状态的消息,实现幂等逻辑;
- 生产者:调用
- 适用场景:跨服务/跨库的最终一致性场景(电商、支付、金融、数据同步);
- 避坑关键 :
- 监听器与生产者组一致;
- 回查方法返回明确状态;
- 消费者实现幂等;
- 避免本地事务执行时间过长。
RocketMQ 事务消息不保证「强一致性」,但能保证「最终一致性」,是分布式系统中解决跨服务事务的首选方案,相比 Seata 等框架,更轻量、无侵入,适合中小规模分布式场景。