RocketMq消息篇(顺序、定时、事务)

223 阅读7分钟

定时消息(4.x)

在 RocketMQ 4.x 及更早版本中,延迟消息仅支持预设的 18 个固定延迟级别(如 1s、5s、10s、30s 等),用户无法自定义任意延迟时间。
实现原理:每个延迟级别对应一个独立的队列(SCHEDULE_TOPIC_XXXX 主题下的队列),消息被暂存到指定延迟级别的队列中,由后台任务扫描后投递到目标 Topic。

定时消息(5.x)

RocketMQ 5.0 通过 时间轮(Timing Wheel)算法 和 动态延迟队列 实现了 任意精度的延迟消息,核心特性如下:

(1) 任意延迟时间支持

  • 自定义延迟时长:用户可为消息指定任意的延迟时间(例如 2.5 秒、1 小时 23 分钟等),无需受限于固定级别。
  • 精度范围:支持毫秒级(ms)的延迟精度,理论最小延迟为 1ms。

(2) 实现原理

  • 时间轮调度

    • 时间轮是一个高效的时间管理数据结构,将延迟时间划分为多个槽(slot),每个槽对应一个时间区间。
    • 消息根据延迟时间被分配到对应的槽中,定时任务按时间轮刻度推进,触发到期消息的投递。
  • 动态队列分配

    • 不再依赖固定数量的队列,而是根据消息的延迟时间动态分配存储位置,减少资源浪费。

延迟消息代码示例

生产者代码:

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

public class RocketMQ5ArbitraryDelayProducer {
    public static void main(String[] args) throws Exception {
        // 创建生产者实例
        DefaultMQProducer producer = new DefaultMQProducer("arbitrary_delay_producer_group");
        // 设置NameServer地址
        producer.setNamesrvAddr("localhost:9876");
        // 启动生产者
        producer.start();

        // 创建消息实例,指定主题、标签和消息体
        Message message = new Message("ArbitraryDelayTopic", "TagA", "Hello, Arbitrary Delay Message".getBytes(StandardCharsets.UTF_8));
        // 设置延迟时间为 30 秒
        long delayTimeMs = TimeUnit.SECONDS.toMillis(30);
        long deliverTimestamp = System.currentTimeMillis() + delayTimeMs;
        message.putUserProperty("DELAY", String.valueOf(deliverTimestamp));

        // 发送消息
        SendResult sendResult = producer.send(message);
        System.out.printf("Send result: %s%n", sendResult);

        // 关闭生产者
        producer.shutdown();
    }
}

消费者代码:

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;

public class RocketMQ5ArbitraryDelayConsumer {
    public static void main(String[] args) throws InterruptedException, MQClientException {
        // 创建消费者实例
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("arbitrary_delay_consumer_group");
        // 设置NameServer地址
        consumer.setNamesrvAddr("localhost:9876");
        // 订阅主题
        consumer.subscribe("ArbitraryDelayTopic", "*");
        // 注册消息监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), new String(msg.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 启动消费者
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

顺序消息

RocketMQ 支持两种顺序消息:全局顺序消息和分区顺序消息。

全局顺序消息

概念

全局顺序消息要求一个 Topic 下的所有消息都按照严格的顺序进行生产和消费。也就是说,对于一个 Topic,所有生产者发送的消息会依次进入一个队列,消费者也会按照相同的顺序依次消费这些消息。

实现步骤

  1. 创建 Topic 时指定单队列:为了保证消息的全局顺序,需要确保 Topic 只包含一个队列。因为如果有多个队列,不同队列之间的消息无法保证顺序。
  2. 生产者发送消息:生产者按照业务顺序依次发送消息到该 Topic。
  3. 消费者消费消息:消费者使用单线程从该队列中消费消息,确保消息按顺序处理。

分区顺序消息

概念

分区顺序消息是指在一个 Topic 下,按照业务的某个维度(如订单 ID、用户 ID 等)将消息划分到不同的队列中,每个队列内部的消息按照顺序生产和消费,但不同队列之间的消息顺序不做保证。这种方式在保证部分顺序的同时,也提高了系统的并发性能。

实现步骤

  1. 生产者发送消息时指定队列选择器:生产者根据业务的某个维度(如订单 ID),通过自定义的队列选择器将消息发送到指定的队列中。
  2. 消费者消费消息:消费者使用单线程从每个队列中消费消息,确保每个队列内部的消息按顺序处理。

事务消息

RocketMQ 事务消息是一种支持分布式事务的消息机制,它能够保证在分布式系统中,消息的发送和本地事务的执行保持一致,常用于解决跨服务数据一致性问题。

核心原理

RocketMQ 事务消息采用两阶段提交的思想,通过引入事务消息状态机和事务消息服务来保证消息的最终一致性,具体流程分为三个阶段:

1. 发送半消息(Half Message)

  • 生产者向 RocketMQ 发送事务消息时,首先会发送一个半消息到 Broker。半消息是一种暂时不能被消费者消费的消息,Broker 接收到半消息后会将其存储,但不会立即将消息的状态告知消费者。
  • 生产者发送半消息成功后,会执行本地事务。

2. 本地事务执行

  • 生产者执行本地事务,例如更新数据库记录、调用其他服务等。
  • 本地事务执行完成后,生产者会根据执行结果向 Broker 发送二次确认消息,确认消息有两种类型:
    • 提交(Commit) :如果本地事务执行成功,生产者向 Broker 发送提交消息,Broker 将半消息标记为可消费状态,消费者可以正常消费该消息。
    • 回滚(Rollback) :如果本地事务执行失败,生产者向 Broker 发送回滚消息,Broker 将删除半消息,消费者不会收到该消息

3. 事务状态回查

  • 如果 Broker 在一定时间内没有收到生产者的二次确认消息,会主动向生产者发起事务状态回查请求。
  • 生产者收到回查请求后,需要检查本地事务的执行状态,并向 Broker 返回最终的事务状态(提交或回滚),Broker 根据返回的状态进行相应的处理。

4. 代码例子

Producer 有一个 TransactionListener 属性,这个由开发者通过实现这个接口来自己定义。这个接口有两个方法:

  • 提交本地事务 executeLocalTransaction
  • 检查本地事务状态 checkLocalTransaction
// 设置事务监听器
producer.setTransactionListener(new TransactionListener() {
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            // 模拟本地事务执行
            System.out.println("Executing local transaction...");
            // 这里可以执行具体的业务逻辑,如数据库操作
            // 假设本地事务执行成功
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (Exception e) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        // 事务状态回查,检查本地事务的执行状态
        System.out.println("Checking local transaction state...");
        // 这里可以根据业务逻辑检查本地事务的状态
        return LocalTransactionState.COMMIT_MESSAGE;
    }
});

Broker 保存 half 消息时,把消息 topic 改为 RMQ_SYS_TRANS_HALF_TOPIC,然后把消息投递到 queueId 等于 0 的队列。投递成功后给 Producer 返回 PutMessageStatus.PUT_OK。

Broker 初始化的时候,会初始化一个 TransactionalMessageServiceImpl 线程,这个线程会定时检查过期的消息,通过向 Producer 发送 check 消息来获取事务状态。

检查事务消息的流程如下:

image.png Producer 收到 check 消息后,最终调用 TransactionListener 中定义的 checkLocalTransaction 方法,查询本地事务执行状态,然后发送给 Broker。

需要注意的是,check 消息发送给 Broker 时,会在请求 Header 中给 fromTransactionCheck 属性赋值为 true,以标记是 check 消息。

这里有两个参数需要注意:

  • 事务消息超时时间,超时后会向 Producer 发送 check 消息检查本地事务状态,默认 6s;
  • 最大检查次数,Broker 每次向 Producer 发送 check 消息后检查次数加 1,超过最大检查次数后 half 消息被丢弃,默认最大检查次数是 15;注意:这里的丢弃是把消息写入了一个新的队列,Topic 为 TRANS_CHECK_MAX_TIME_TOPIC,queueId 为 0。
  • 文件保存时间,默认72 小时。