事务消息
实现流程
- 创建生产者
- 创建一个事务监听器,实现 TransactionListener 接口和下面两个方法
executeLocalTransaction:执行本地事务,也就是要和发送消息同时成功业务处理checkLocalTransaction:检查本地是服务是否成功
- 给生产者绑定事务监听器
- 生产者发送消息
执行流程
- 发送事务消息
- 执行本地事务
TransactionListener#executeLocalTransaction() - 回查本地事务执行结果状态
TransactionListener#checkLocalTransaction() - 对不同本地事务的状态作对应处理
实现原理
2PC 思想
- 一阶段:半消息发送与本地事务执行
- 二阶段:事务提交/回滚与消息投递
半消息发送(一阶段)
发送事务消息时,消息发送至一个特殊的 Topic(RMQ_SYS_TRANS_HALF_TOPIC)
不是目标 Topic 所以此时消息对消费者不可见,这时消息是一个 半消息
半消息的作用是探测 Broker 的是否可用
本地事务执行(一阶段)
触发流程:生产者收到 ACK 后,执行本地事务;若 ACK 失败,终止流程(不执行本地事务)
事务监听器:返回本地事务状态(三选一)
COMMIT_MESSAGE:本地事务成功,提交消息ROLLBACK_MESSAGE:本地事务失败,回滚消息UNKNOW:状态未知,用于触发后续回查(默认间隔1分钟,最多15次)
事务状态处理阶段(二阶段)
Broker 发起事务回查,根据本地事务不同状态做出不同处理
- COMMIT_MESSAGE:将半消息从
RMQ_SYS_TRANS_HALF_TOPIC转移到目标 Topic,这时对于消费者就可见了 - ROLLBACK_MESSAGE:直接删除半消息
- UNKNOW:等待下一次回查;超过最大次数后消息回滚(删除半消息)
流程图
%% RocketMQ 事务消息流程图
sequenceDiagram
participant P as 生产者
participant B as Broker
participant C as 消费者
%% 第一阶段:发送半消息
P->>B: 1. 发送半消息(RMQ_SYS_TRANS_HALF_TOPIC)
B-->>P: 2. 返回ACK(写入成功/失败)
%% 第二阶段:执行本地事务
alt ACK成功
P->>P: 3. 执行本地事务
P->>B: 4. 提交事务状态(COMMIT/ROLLBACK/UNKNOW)
else ACK失败
P->>P: ❌ 终止流程(不执行事务)
end
%% 第三阶段:Broker处理
alt 状态=COMMIT
B->>B: 5. 将消息移至真实Topic
B->>C: 6. 投递消息给消费者
else 状态=ROLLBACK
B->>B: 5. 删除半消息
else 状态=UNKNOW
loop 每隔1分钟回查(最多15次)
B->>P: 7. 发起事务回查
P-->>B: 8. 返回最终状态
end
end
使用示例
事务监听器
checkBusinessStatus 方法可以优化,再执行本地事务时,如果成功把执行结果写入 redis,后续回查时直接查 redis 就行了
// 实现 TransactionListener 接口
public class TransactionListenerImpl implements TransactionListener {
// 执行本地事务
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
System.out.printf("执行本地事务: %s%n", new String(msg.getBody()));
try {
// 模拟业务处理
boolean success = doBusiness();
return success ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE;
} catch (Exception e) {
return LocalTransactionState.UNKNOW;
}
}
// 本地事务状态回查
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.printf("回查事务状态: %s%n", new String(msg.getBody()));
// 模拟回查业务状态(自行实现检查数据逻辑)
// enum BusinessStatus { SUCCESS, FAILED, PROCESSING }
BusinessStatus status = checkBusinessStatus(msg.getTransactionId());
switch (status) {
case SUCCESS -> return LocalTransactionState.COMMIT_MESSAGE;
case FAILED -> return LocalTransactionState.ROLLBACK_MESSAGE;
default -> return LocalTransactionState.UNKNOW;
}
}
}
生产者
// 1. 创建事务监听器
TransactionListener transactionListener = new TransactionListenerImpl();
// 2. 创建生产者并设置事务监听器
TransactionMQProducer producer = new TransactionMQProducer("transaction_producer_group");
producer.setNamesrvAddr("localhost:9876");
producer.setTransactionListener(transactionListener);
producer.setCheckTransactionMaxTimes(20); // 设置最大回查次数
producer.setCheckTransactionInterval(30000); // 设置回查间隔(毫秒)
// 3. 启动生产者
producer.start();
// 4. 发送事务消息
Message msg = new Message("TransactionTopic", "TagA", "Hello Transaction Message ".getBytes());
TransactionSendResult result = producer.sendMessageInTransaction(msg, null);
System.out.printf("发送事务消息结果: %s%n", result);
// 5. 保持运行(生产环境不需要)
Thread.sleep(100000);
// 6. 关闭生产者
producer.shutdown();
消费者没特别的地方,正常编码即可
注意事项
- 本地事务实行失败,需要再事务回查中作相应的处理,比如可能有要回滚的数据等
- 发送半消息未收到
ACK(Broker 不可用 或 网络波动)- 半消息其实就是一个普通的消息,只是发送到一个特殊的 Topic
- RokcetMQ 可以给生产者配置重试机制(默认的是超时时间3s,重发次数时2次)
- 所以半消息发送不成功(未收到
ACK)生产者会重试,如果是网络波动可能消息重复投递,消费者要做好消费幂等
- 回查本地事务的间隔和次数是可配置的,如果最大回查次数后任然是 UNKNOW,半消息会被丢弃
- 事务消息只能保证本地事务和消息投递能同时成功,但是消息消费可能失败,这时本地事务是不会回滚的

浙公网安备 33010602011771号