事务消息

实现流程

  1. 创建生产者
  2. 创建一个事务监听器,实现 TransactionListener 接口和下面两个方法
    • executeLocalTransaction:执行本地事务,也就是要和发送消息同时成功业务处理
    • checkLocalTransaction:检查本地是服务是否成功
  3. 给生产者绑定事务监听器
  4. 生产者发送消息

执行流程

  1. 发送事务消息
  2. 执行本地事务 TransactionListener#executeLocalTransaction()
  3. 回查本地事务执行结果状态 TransactionListener#checkLocalTransaction()
  4. 对不同本地事务的状态作对应处理

实现原理

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();

消费者没特别的地方,正常编码即可

注意事项

  1. 本地事务实行失败,需要再事务回查中作相应的处理,比如可能有要回滚的数据等
  2. 发送半消息未收到 ACK(Broker 不可用 或 网络波动)
    • 半消息其实就是一个普通的消息,只是发送到一个特殊的 Topic
    • RokcetMQ 可以给生产者配置重试机制(默认的是超时时间3s,重发次数时2次)
    • 所以半消息发送不成功(未收到 ACK)生产者会重试,如果是网络波动可能消息重复投递,消费者要做好消费幂等
  3. 回查本地事务的间隔和次数是可配置的,如果最大回查次数后任然是 UNKNOW,半消息会被丢弃
  4. 事务消息只能保证本地事务和消息投递能同时成功,但是消息消费可能失败,这时本地事务是不会回滚的
posted @ 2025-07-03 15:36  CyrusHuang  阅读(16)  评论(0)    收藏  举报