RocketMQ 事务消息:详细使用案例(含完整代码与最佳实践)
目标:让“发送 MQ 消息”和“本地数据库事务”最终一致,并且能在各种异常(宕机/超时/网络抖动)下做到可恢复、可兜底。
1. 事务消息适用场景(别乱用)
RocketMQ 事务消息解决的是:“本地事务 + 发消息”跨资源一致性(最终一致)。
典型场景:
- 创建订单成功后,发送“订单已创建”事件给下游(库存、积分、营销、风控…)
- 支付成功落库后,发送“支付成功”事件给履约/发货系统
- 账户入账成功后,发送“入账完成”事件给对账/通知系统
不适合:
- 需要强一致/原子性(那是分布式事务/共识协议的范畴)
- 只想“可靠投递”但不涉及本地事务:用 同步发送 + 重试 + 生产者确认 就够了
2. RocketMQ 事务消息的核心机制(你必须搞懂)
它是一个“近似 2PC”的流程(但不是 XA):
- Producer 先发一条 Half Message(半消息) 到 Broker
- 这条消息 对 Consumer 不可见
- Producer 执行 本地事务
- Producer 根据本地事务结果,向 Broker 发送:
- COMMIT:Broker 才把消息投递给 Consumer
- ROLLBACK:Broker 丢弃这条半消息
- 如果 Broker 长时间没等到 COMMIT/ROLLBACK(比如 Producer 宕机),会触发 事务回查(Transaction Check)
- Broker 回调 Producer 的
checkLocalTransaction,让 Producer 根据本地事务最终状态补一个 COMMIT/ROLLBACK
- Broker 回调 Producer 的
2.1 状态机(非常重要)
| 状态 | 含义 | 消费者是否可见 |
|---|---|---|
| Half | 半消息已到 Broker | 否 |
| Commit | 本地事务成功,提交消息 | 是 |
| Rollback | 本地事务失败,回滚消息 | 否 |
| Unknown | Producer 暂时不确定,等待回查 | 否 |
3. 设计原则:事务消息 = “业务事件” + “事务日志/状态表”
事务消息能兜底的前提是:回查时你能判断本地事务到底成没成。
最佳实践:在本地库里落一张 事务日志表(或业务状态字段)。
- 本地事务成功:把状态更新为
SUCCESS - 本地事务失败:
FAIL - 回查逻辑:只查本地确定性状态(DB),不要依赖内存变量
4. 案例:创建订单(本地事务落库)后发送“订单创建事件”
4.1 数据表(示例)
-- 订单表(简化)
CREATE TABLE t_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL UNIQUE,
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status VARCHAR(32) NOT NULL, -- CREATED / FAILED ...
created_at DATETIME NOT NULL
);
-- 事务消息日志表(强烈建议)
CREATE TABLE t_tx_msg_log (
tx_id VARCHAR(128) PRIMARY KEY, -- 事务ID(建议用 msgId 或业务生成)
biz_key VARCHAR(128) NOT NULL, -- 业务键,比如 order_no
topic VARCHAR(128) NOT NULL,
tag VARCHAR(128) NULL,
state VARCHAR(32) NOT NULL, -- SUCCESS / FAIL
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
CREATE INDEX idx_tx_msg_log_biz_key ON t_tx_msg_log(biz_key);
为什么要 tx_msg_log?
因为回查时你只要查这张表就能知道要 commit 还是 rollback。
5. 方案 A:使用原生 RocketMQ Client(最清晰、最可控)
适合你要深入掌控事务回查、线程池、性能参数的场景。
5.1 Maven 依赖(示例)
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>5.x 或 4.9.x(按你们集群对齐)</version>
</dependency>
5.2 Producer:TransactionMQProducer + TransactionListener
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
public class OrderTxProducer {
private final TransactionMQProducer producer;
public OrderTxProducer(String producerGroup, String nameSrvAddr,
TransactionListener listener) throws MQClientException {
this.producer = new TransactionMQProducer(producerGroup);
producer.setNamesrvAddr(nameSrvAddr);
// 事务回查线程池(Broker 回查会走这里)
ExecutorService executor = new ThreadPoolExecutor(
2, 8, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2000),
r -> new Thread(r, "tx-check-thread"));
producer.setExecutorService(executor);
producer.setTransactionListener(listener);
producer.start();
}
public TransactionSendResult sendOrderCreatedInTransaction(String topic, String tag,
String orderNo, String jsonBody) throws Exception {
Message msg = new Message(topic, tag, orderNo,
jsonBody.getBytes(StandardCharsets.UTF_8));
// args 会透传给 executeLocalTransaction
return producer.sendMessageInTransaction(msg, orderNo);
}
}
5.3 本地事务执行 + 回查:TransactionListener 实现
关键点:
executeLocalTransaction:执行本地事务(插入订单、写 tx_log),然后返回 COMMIT/ROLLBACK/UNKNOWNcheckLocalTransaction:被 Broker 回查时执行,根据 tx_log 判断最终状态(必须幂等、可重入)
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
public class OrderTransactionListener implements TransactionListener {
private final OrderService orderService; // 你的业务服务(含 DB 事务)
private final TxMsgLogService txMsgLogService;
public OrderTransactionListener(OrderService orderService, TxMsgLogService txMsgLogService) {
this.orderService = orderService;
this.txMsgLogService = txMsgLogService;
}
/**
* 发送 half message 成功后,立刻回调这里执行本地事务
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String orderNo = (String) arg;
String txId = msg.getTransactionId(); // RocketMQ 给的 txId(可用)
try {
// 1) 执行本地事务(建议:订单 + tx_log 同一个本地事务)
orderService.createOrderWithTxLog(orderNo, txId, msg.getTopic(), msg.getTags(), new String(msg.getBody()));
// 2) 本地事务成功 -> COMMIT
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
// 3) 本地事务失败 -> 记录失败(或直接不记录也行,但回查要能判断)
txMsgLogService.markFailIfAbsent(txId, orderNo, msg.getTopic(), msg.getTags());
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
/**
* Broker 没收到 COMMIT/ROLLBACK 会回查这里
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String txId = msg.getTransactionId();
TxState state = txMsgLogService.queryState(txId);
if (state == TxState.SUCCESS) {
return LocalTransactionState.COMMIT_MESSAGE;
}
if (state == TxState.FAIL) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
// 查不到/未落库:返回 UNKNOWN,Broker 之后会再回查(有上限)
return LocalTransactionState.UNKNOW;
}
}
5.4 OrderService:把“订单落库 + tx_log”放同一个本地事务
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class OrderService {
private final JdbcTemplate jdbcTemplate;
public OrderService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Transactional
public void createOrderWithTxLog(String orderNo, String txId, String topic, String tag, String body) {
// 1) 插入订单
jdbcTemplate.update(
"INSERT INTO t_order(order_no, user_id, amount, status, created_at) VALUES (?, ?, ?, ?, ?)",
orderNo, 1001L, new BigDecimal("99.00"), "CREATED", LocalDateTime.now()
);
// 2) 插入事务日志(SUCCESS)
jdbcTemplate.update(
"INSERT INTO t_tx_msg_log(tx_id, biz_key, topic, tag, state, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
txId, orderNo, topic, tag, "SUCCESS", LocalDateTime.now(), LocalDateTime.now()
);
}
}
注意:如果你希望“订单成功但 tx_log 写入失败”也能被回查识别,那你就需要用订单状态字段(status=CREATED)作为回查依据,或者 tx_log 写入要足够可靠。
6. 方案 B:Spring Boot + rocketmq-spring(更常用、开发更快)
底层机制一样:先 half message,再本地事务,再 commit/rollback,失败就回查。
6.1 依赖(示例)
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>按你们项目选</version>
</dependency>
6.2 配置(application.yml 示例)
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: order-tx-producer-group
send-message-timeout: 3000
6.3 发送事务消息:RocketMQTemplate.sendMessageInTransaction
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
@Service
public class OrderTxSendService {
private final RocketMQTemplate rocketMQTemplate;
public OrderTxSendService(RocketMQTemplate rocketMQTemplate) {
this.rocketMQTemplate = rocketMQTemplate;
}
public void createOrderAndSendEvent(String orderNo, String bodyJson) {
// destination 格式:topic:tag
String destination = "ORDER_TOPIC:CREATED";
org.springframework.messaging.Message<String> msg =
MessageBuilder.withPayload(bodyJson)
.setHeader("KEYS", orderNo) // 业务 key,便于排查
.build();
// arg 会透传到本地事务执行
rocketMQTemplate.sendMessageInTransaction(destination, msg, orderNo);
}
}
6.4 本地事务监听器:@RocketMQTransactionListener
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;
@Component
@RocketMQTransactionListener(txProducerGroup = "order-tx-producer-group")
public class OrderTxListener implements RocketMQLocalTransactionListener {
private final OrderService orderService;
private final TxMsgLogService txMsgLogService;
public OrderTxListener(OrderService orderService, TxMsgLogService txMsgLogService) {
this.orderService = orderService;
this.txMsgLogService = txMsgLogService;
}
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String orderNo = (String) arg;
try {
// 实战建议:把 txId/orderNo 写入 payload 或自定义 header,回查一定拿得到
String txId = String.valueOf(msg.getHeaders().get("TRANSACTION_ID"));
orderService.createOrderWithTxLog(orderNo, txId, "ORDER_TOPIC", "CREATED", String.valueOf(msg.getPayload()));
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String txId = String.valueOf(msg.getHeaders().get("TRANSACTION_ID"));
TxState state = txMsgLogService.queryState(txId);
if (state == TxState.SUCCESS) return RocketMQLocalTransactionState.COMMIT;
if (state == TxState.FAIL) return RocketMQLocalTransactionState.ROLLBACK;
return RocketMQLocalTransactionState.UNKNOWN;
}
}
⚠️ 注意:rocketmq-spring 里 Header key(比如
TRANSACTION_ID)在不同版本可能不一样。
实战里最稳的做法:你自己传 txId/orderNo(payload 或 header),别赌框架内部字段。
7. Consumer 端怎么写(以及为什么必须幂等)
事务消息保证的是:只在本地事务成功后才会投递。
但 Consumer 仍然可能收到重复消息(重投/网络抖动/消费超时)。
7.1 幂等去重(推荐用唯一键)
做法之一:消费记录表 / 去重键(建议用 orderNo 或 msgKey)。
CREATE TABLE t_consume_dedup (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
msg_key VARCHAR(128) NOT NULL UNIQUE,
consumed_at DATETIME NOT NULL
);
消费逻辑(伪代码):
public void onMessage(OrderCreatedEvent event) {
// 1) 先插 dedup 表(利用唯一键防重复)
// 2) 插成功 -> 执行业务(扣库存、发券…)
// 3) 插失败(duplicate key) -> 直接 return
}
8. 事务回查的“坑”和“调优点”(面试也爱问)
8.1 为什么会回查?
- Producer 发送 half message 成功
- 但 Producer:
- 本地事务执行完了,还没来得及 commit/rollback 就挂了
- 或网络问题导致 commit/rollback 没送到 Broker
- 或 commit/rollback 超时
8.2 回查函数必须做到:
- 快:别在里面做复杂 IO/长事务
- 可重入:可能并发、可能重复调用
- 只依赖本地确定性状态(DB 状态/日志表),别依赖内存变量
9. 排错 Checklist(线上排障直接用)
- half message 到 Broker 了吗?(看 send result / 控制台)
- 本地事务到底有没有成功?(看订单表、tx_log 表)
- Producer 是否发生回查?(
checkLocalTransaction是否被调用) - Consumer 是否幂等?(重复消费是否会造成二次扣减)
- 消息 Key 是否合理?(建议用
orderNo当 key,排查快)
10. 推荐落地模板(最稳组合)
- Producer:
- 事务消息
- 本地事务里:业务落库 + tx_log(SUCCESS/FAIL)(或业务状态字段)
- 回查:只查 tx_log(或业务状态)
- Consumer:
- 强制幂等
- 失败重试可控(必要时转 DLQ + 补偿)
11. 常见问答(快问快答)
Q:事务消息能保证 100% 不丢吗?
A:它保证“只在本地事务成功后才对外投递”,并通过回查解决“提交状态不确定”。最终一致还得靠你:本地状态可回查 + 消费幂等兜底。
Q:executeLocalTransaction 返回 UNKNOWN 会怎样?
A:消息不会被消费,Broker 会过一段时间回查 checkLocalTransaction。
Q:回查时查不到 tx_log 怎么办?
A:返回 UNKNOWN,让它再回查;同时你要排查为什么没落库(本地事务失败/DB 异常)。达到最大回查次数后就需要你们的兜底补偿机制。

被折叠的 条评论
为什么被折叠?



