【RocketMQ】事务消息详解
- [【一】什么是 RocketMQ 事务消息](#【一】什么是 RocketMQ 事务消息)
- 【二】使用场景
- 【三】底层原理
- 【四】优缺点
- 【五】替换方案(分布式事务方案对比)
- [【六】代码详细案例(SpringBoot + RocketMQ 事务消息)](#【六】代码详细案例(SpringBoot + RocketMQ 事务消息))
-
- 【1】maven依赖
- 【2】application.yml
- [【3】实现 RocketMQLocalTransactionListener](#【3】实现 RocketMQLocalTransactionListener)
- 【4】生产者发送事务消息
- [【5】OrderService 业务层](#【5】OrderService 业务层)
- 【6】消费者(库存服务,注意消费端必须做幂等)
- 【七】注意事项
【一】什么是 RocketMQ 事务消息
RocketMQ 事务消息用于解决分布式事务问题,实现本地数据库事务与消息发送的原子性:
要么本地事务执行成功 + 消息投递成功;要么本地事务失败 + 消息完全不投递;避免出现:数据库成功消息没发 / 消息发出去数据库失败这种数据不一致。
总结:RocketMQ 事务消息保证生产端的事务和消息投递状态同步,要么都成功,要么都失败。
注意:RocketMQ 事务消息不能保证消费端事务原子性,只保证「发送消息 + 本地事务」的原子性;消费端还需要自己做幂等。
【二】使用场景
核心场景:跨系统 / 跨服务,本地 DB 操作要和发消息绑定原子性
【1】订单创建后扣减库存
订单服务创建订单(本地事务),同时发送消息给库存服务扣库存。
如果订单入库成功,消息没发出去 → 库存不减,订单处于已创建无扣减,数据不一致;
如果消息先发,订单入库失败 → 库存扣了,订单没生成。
用事务消息:订单创建成功消息对外可见;订单回滚,消息直接丢弃。
【2】支付完成,发送通知、积分、优惠券
支付服务本地更新支付记录,同时发消息:给用户加积分、发优惠券、推送短信。
支付成功才允许消息被消费;支付失败消息不能被消费。
【3】业务流程异步解耦,需要强一致性
业务 A 完成本地数据库操作后,触发下游多个异步业务,不能出现下游收到消息但上游数据库回滚。
【4】分布式链路中,本地事务触发下游异步调用
比如:用户注册成功,初始化账户、生成账单,多个下游服务异步处理。
❌ 不适合场景
(1)需要多服务数据库同时原子提交(2PC 那种强一致性),事务消息最终一致性,不是强一致;
(2)短同步链路,不需要异步;
(3)追求极低延迟:事务消息有半消息、回查逻辑,会有额外耗时。
【三】底层原理
RocketMQ 事务消息分为 半消息 (Prepare 消息) → 执行本地事务 → 提交 / 回滚消息 → 事务回查
【1】完整流程
(1)发送半消息(Prepare 消息)
Producer 发送事务半消息给 Broker;Broker 收到消息标记为 TRANSACTION_PREPARED,这条消息对消费者不可见,消费者消费不到。返回成功给生产者。
半消息:消息已经存 broker,但是不会投递消费队列。
(2)Producer 执行本地事务
收到半消息发送成功响应后,Producer 执行自己本地数据库事务(执行业务逻辑)。本地事务执行结果三种状态:
(1)COMMIT_MESSAGE:本地事务成功 → 通知 Broker 提交消息,消息正式可见,消费者可以消费
(2)ROLLBACK_MESSAGE:本地事务失败 → 通知 Broker 回滚,消息直接删除,永远不会被消费
(3)UNKNOWN:状态未知(网络抖动、生产者宕机,没返回 commit/rollback),Broker 需要事务回查
(3)Broker 事务回查(核心容错机制)
当 Broker 收到半消息,长时间没有收到 commit/rollback 指令;Broker 会定时主动回调 Producer 的checkLocalTransaction接口,查询本地事务当前真实状态。
(1)如果本地事务已经 commit → Broker 提交消息
(2)如果本地事务已经 rollback → Broker 删除消息
(3)如果还是 UNKNOWN,等待下一次回查,回查有最大次数,超过直接回滚丢弃。
⚠️关键点:回查依赖业务实现检查本地事务状态接口;如果生产者宕机重启,重启后 Broker 依旧会来回查。
【2】存储层面
(1)半消息实际存储在 RMQ_SYS_TRANS_HALF_TOPIC 这个系统 topic,不是业务 topic;
(2)commit 之后,消息复制到真正业务 topic,对外可见;
(3)rollback 直接删除半消息;
(4)事务回查 offset 存储在 RMQ_SYS_TRANS_OP_HALF_TOPIC。
【3】时序图文字版
bash
Producer Broker
| sendHalfMessage ------>| 保存半消息,TRANSACTION_PREPARED,不可消费
|<-------send ok -------- |
| 执行本地DB事务
|
|----commit/rollback----->| 更新消息状态
| | commit:投递真实topic;rollback删除
# 如果生产者宕机,没返回commit/rollback
Broker定时轮询半消息
Broker --回调checkLocalTransaction--> Producer
Producer查询数据库本地事务状态返回
Broker根据返回结果提交/回滚
【四】优缺点
✅优点
(1)实现本地事务与消息发送原子性,保证上游业务 DB 成功消息才对外可见;
(2)异步解耦,下游业务异步执行,提升吞吐量;
(3)有事务回查机制,生产者宕机重启后依然可以完成事务状态确认,容错能力强;
(4)基于 RocketMQ 原生支持,不需要额外引入中间件。
❌缺点
(1)只保证发送端原子性,消费端不保证。消费失败需要消费端重试 + 幂等;
(2)需要业务实现本地事务状态检查接口,业务侵入;业务必须提供查询本地事务执行状态的能力;
(3)事务回查会带来一定延迟,不适合极致低延迟场景;
(4)如果业务没有做好幂等,多次回查 + 消息重试会导致重复消费;
(5)不适合跨多个服务本地事务(仅保证生产者本地 DB + 发消息原子;不能同时保证 A、B 两个服务 DB 同时原子);
(6)半消息会占用 broker 存储;大量事务消息会增加 Broker 压力;
(7)回查频率、最大回查次数需要合理调参,设置不当会出现消息悬停。
常见坑:业务没有实现 checkLocalTransaction,或者查询逻辑写错,导致消息一直回查,最后丢弃,业务消息丢失。
【五】替换方案(分布式事务方案对比)

最常用替代:本地消息表方案,很多生产环境不使用 RocketMQ 事务消息,而是自己实现本地消息表,避免 RocketMQ 事务消息的回查、半消息的各种坑。
替代方案示例:本地消息表
很多项目不使用 RocketMQ 事务消息,选择本地消息表。原理:同一个本地事务,同时写入业务数据 + message_record 消息表;然后定时任务扫描消息表发送 MQ,更新消息状态。
bash
id biz_id topic content status retry_count create_time
1 order_1001 ORDER_CREATE_TOPIC xxx 0 待发送 0 2026‑08‑19
status:0 待发送;1 发送成功;2 发送失败
java
@Transactional
public void createOrderWithMsg(Long orderId){
//1.本地事务内,插入订单
orderMapper.insert(order);
//2.同一个事务,插入消息记录,状态=待发送
MessageRecord record = new MessageRecord();
record.setBizId(orderId.toString());
record.setTopic("ORDER_CREATE_TOPIC");
record.setContent("xxx");
record.setStatus(0);
messageRecordMapper.insert(record);
}
定时任务:扫描 status=0,重试次数少的消息,发送 MQ;发送成功更新 status=1;失败重试 + retry_count++。
如果发送失败,定时任务不断重试,直到发送成功。
优点:不依赖 MQ 事务能力,任意 MQ 都能用;逻辑可控;缺点:需要维护一张消息表,定时任务。
【六】代码详细案例(SpringBoot + RocketMQ 事务消息)
【1】maven依赖
xml
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
【2】application.yml
yml
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: ORDER_TRANS_PRODUCER_GROUP
业务场景:创建订单(本地数据库事务),事务消息发送,库存服务消费消息扣库存。
数据库表:t_order(订单表),order_id 作为事务唯一 id。
【3】实现 RocketMQLocalTransactionListener
两个核心方法:
executeLocalTransaction:半消息发送成功后,执行本地事务
checkLocalTransaction:Broker 回查,检查本地事务状态
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(txProducerGroup = "ORDER_TRANS_PRODUCER_GROUP")
@Component
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
// 注入自己的service,操作数据库
private final OrderService orderService;
public OrderTransactionListener(OrderService orderService) {
this.orderService = orderService;
}
/**
* 半消息发送成功之后,执行本地事务
* @param msg mq消息
* @param arg 业务自定义参数,这里传入orderId
* @return
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
Long orderId = (Long) arg;
try {
// 执行本地数据库事务:创建订单
orderService.createOrder(orderId);
// 本地事务成功,通知Broker提交消息,消息对消费者可见
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
// 本地事务失败,回滚,消息删除
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* Broker事务回查:生产者宕机,没有返回commit/rollback,broker回调这个接口查询本地事务状态
* 重点:根据消息拿到业务唯一id,查询数据库,判断本地事务到底成功还是失败
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
// 从消息header取出orderId
String orderIdStr = (String) msg.getHeaders().get("ORDER_ID");
Long orderId = Long.parseLong(orderIdStr);
// 查询数据库,看订单是否创建成功
boolean exist = orderService.isOrderExist(orderId);
if(exist){
// DB已经成功,提交消息
return RocketMQLocalTransactionState.COMMIT;
}else{
// DB没有数据,事务回滚
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
【4】生产者发送事务消息
java
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
@Service
public class OrderMqService {
private final RocketMQTemplate rocketMQTemplate;
public static final String ORDER_TOPIC = "ORDER_CREATE_TOPIC";
public OrderMqService(RocketMQTemplate rocketMQTemplate) {
this.rocketMQTemplate = rocketMQTemplate;
}
public void sendOrderCreateTxMsg(Long orderId){
Message<String> message = MessageBuilder
.withPayload("订单创建消息,orderId="+orderId)
.setHeader("ORDER_ID",orderId.toString())
.build();
//发送事务消息,第二个参数是业务参数,会传给executeLocalTransaction的arg
rocketMQTemplate.sendMessageInTransaction(
ORDER_TOPIC,
message,
orderId
);
}
}
【5】OrderService 业务层
java
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOrder(Long orderId){
//本地事务:插入订单
Order order = new Order();
order.setOrderId(orderId);
order.setStatus(1);
orderMapper.insert(order);
// 模拟异常,测试回滚
// int i = 1/0;
}
//给事务回查接口调用,查询事务状态
public boolean isOrderExist(Long orderId){
return orderMapper.selectById(orderId) != null;
}
}
【6】消费者(库存服务,注意消费端必须做幂等)
⚠️重点:RocketMQ 事务消息只保证发送端原子;消费端会重试,必须做幂等,用 orderId 判断是否已经扣过库存。
java
@RocketMQMessageListener(topic = "ORDER_CREATE_TOPIC", consumerGroup = "STOCK_CONSUMER_GROUP")
@Component
public class StockConsumer implements RocketMQListener<String> {
private final StockService stockService;
public StockConsumer(StockService stockService) {
this.stockService = stockService;
}
@Override
public void onMessage(String message) {
//解析orderId
Long orderId = parseOrderId(message);
//幂等判断:如果该订单已经扣过库存,直接return,不再扣减
if(stockService.hasDeduct(orderId)){
return;
}
//执行扣库存
stockService.deductStock(orderId);
}
private Long parseOrderId(String message){
//业务自行解析
return 1L;
}
}
【七】注意事项
(1)checkLocalTransaction回查接口必须保证幂等、查询数据库不能查缓存,要查真实 DB 状态;
(2)消费端一定要做幂等,事务消息解决发送端原子,消费失败会重试;
(3)合理设置 Broker 事务回查参数:transactionTimeOut、checkMaxTimes;
(4)不要把业务耗时很长的逻辑放在executeLocalTransaction,半消息超时会触发大量回查;
(5)事务 producer group 必须唯一,一个 group 对应一组事务监听;
(6)如果回查多次还是 UNKNOWN,broker 会直接回滚丢弃消息,业务要监控事务消息丢弃告警。
常见问题
Q:RocketMQ 事务消息能保证消费一定成功吗?
A:不能。只能保证消息一定投递到 broker,上游 DB 成功消息才可见;消费失败会重试,消费成功需要业务自己保证。
Q:半消息会占用磁盘吗?
A:会,存储在系统半消息 topic,commit/rollback 之后才清理。
Q:和本地消息表怎么选?
A:简单业务可以用 RocketMQ 事务消息;复杂生产系统,追求可控性,优先本地消息表。