RocketMQ从实战到源码:客户端消息机制(万字篇幅)

156 阅读29分钟

RocketMQ从实战到源码:客户端消息机制

😄生命不息,写作不止

🔥 继续踏上学习之路,学之分享笔记

👊 总有一天我也能像各位大佬一样

🏆 博客首页   @怒放吧德德  To记录领地 @一个有梦有戏的人

🌝分享学习心得,欢迎指正,大家一起学习成长!

转发请携带作者信息  @怒放吧德德(掘金) @一个有梦有戏的人(CSDN)

rocketmq封面.png

前言

前面一篇文章,我们简单学习了 RockerMQ 的安装以及简单 demo,现在我们就进一步学习 RockerMQ 的消息机制。本篇篇幅较大,内容较多,后续会将每个消息机制进行抽取。

1 基础使用

通过 rocket 的代码中的 example 案例,里面有简单的例子。(包:org.apache.rocketmq.example.quickstart)

生产者代码:

public class Producer {

    /**
     * The number of produced messages.
     */
    public static final int MESSAGE_COUNT = 1000;
    public static final String PRODUCER_GROUP = "Study1";
    public static final String DEFAULT_NAMESRVADDR = "192.168.109.134:9876";
    public static final String TOPIC = "TopicTest";
    public static final String TAG = "TagA";

    public static void main(String[] args) throws MQClientException, InterruptedException {

        // 创建生产者,指定生产组
        DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);

        // 设置Nameserver地址
        producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);

        // 启动生产者
        producer.start();

        for (int i = 0; i < MESSAGE_COUNT; i++) {
            try {
                // 创建消息体:订阅主题、Tag、消息体
                Message msg = new Message(TOPIC,
                        TAG,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
                );

                // 发送消息
                SendResult sendResult = producer.send(msg, 20 * 1000);
                System.out.printf("%s%n", sendResult);
            } catch (Exception e) {
                e.printStackTrace();
                Thread.sleep(1000);
            }
        }
        // 关闭生产者,释放资源
        producer.shutdown();
    }
}

消费这代码:

public class Consumer {

    public static final String CONSUMER_GROUP = "Study1";
    public static final String DEFAULT_NAMESRVADDR = "192.168.109.134:9876";
    public static final String TOPIC = "TopicTest";

    public static void main(String[] args) throws MQClientException {

        // 创建消费者,指定消费组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);

        // 设置Nameserver地址
        consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);

        // 如果特定的消费群体是全新的,请指定从哪里开始
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

        // 配置订阅主题Topic、Tag
        consumer.subscribe(TOPIC, "*");

        // 设置回调函数,处理消息
        consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
            System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        // 启动消费者,启动后会一直挂着,持续处理消息
        consumer.start();

        System.out.printf("Consumer Started.%n");
    }
}

客户端编程逻辑顺序比较固定。

① 生产者固定步骤

  • 创建生产者,指定生产组
  • 设置Nameserver地址
  • 启动生产者
  • 创建消息体:订阅主题、Tag、消息体
  • 发送消息
  • 关闭生产者,释放资源

② 消费者固定步骤

  • 创建消费者,指定消费组
  • 设置Nameserver地址
  • 如果特定的消费群体是全新的,请指定从哪里开始
  • 配置订阅主题Topic、Tag
  • 设置回调函数,处理消息
  • 启动消费者,启动后会一直挂着,持续处理消息

2 确认消息机制

消息是必先要保证安全性,而这包含了两种,一是要确保消息发送到 Broker 上,二是要确保消费者能够正常从 Broker 消费。RocketMQ 提供了完善的消息确认机制,确保消息的可靠传递。主要分为生产者发送确认和消费者消费确认两大部分。

2.1 生产者端

生产者通过多次重试的机制保证消息正常发送到 Broker。生产者发送消息后,需要确认 Broker 是否成功接收并持久化消息,核心依赖 Broker 的响应结果,不同发送模式的确认方式不同。消息生产者的 3 种消息发送方法:同步发送、异步发送、单向发送(3 种简单代码上文均有提及)。

2.1.1 发送模式与确认逻辑
方式确认方式场景
同步发送生产者阻塞等待 Broker 返回SendResult,通过SendStatus.SEND_OK判断是否成功核心业务(如交易、支付),必须确保消息投递成功
异步发送通过SendCallback回调函数接收结果:onSuccess表示成功,onException表示失败高并发场景,无需阻塞等待结果
单向发送只发送消息,不等待 Broker 响应,无确认机制非核心业务(如日志采集),允许少量丢失

Broker 端的刷盘 / 复制策略会影响确认的可靠性:

  • 刷盘策略:同步刷盘(消息写入后立即刷磁盘,确认慢但可靠)、异步刷盘(默认,写入内存后立即返回确认,性能高);
  • 主从复制:同步复制(主 Broker 需等待从 Broker 同步完成再确认)、异步复制(默认,主 Broker 写入后直接确认)。
2.1.2 单向发送

单向发送的情况下,生产者只管往 Broker 发送消息,并不关心 Broker 端是否有接收到。

// 单向发送(无确认)
producer.sendOneway(message);

sendOneway方式发送消息有个好处就是效率高,但是如果 Broker 没收到的时候是无法补救的,适用日志采集这种形式。

2.1.3 同步发送

同步发送之后会阻塞当前线程,等待 Broker 响应,直到得到响应,开发可以通过获取的结果去做其他处理,如果发送失败可以进行补救,比如重发或者告警之类。

// 同步发送(有确认)
SendResult sendResult = producer.send(message);
System.out.println("发送状态:" + sendResult.getSendStatus()); // SEND_OK

SendResult 是 Broker 返回的信息,可以查看状态是否成功。里面有 SendStatus,用来表示发送状态。

public enum SendStatus {
    SEND_OK,
    FLUSH_DISK_TIMEOUT,
    FLUSH_SLAVE_TIMEOUT,
    SLAVE_NOT_AVAILABLE;

    private SendStatus() {
    }
}

这个枚举的说明如下:

  • SEND_OK: 发送成功,消息已持久化到Broker
  • FLUSH_DISK_TIMEOUT: 刷盘超时(同步刷盘模式)
  • FLUSH_SLAVE_TIMEOUT: 同步到Slave超时(主从模式)
  • SLAVE_NOT_AVAILABLE: Slave不可用

但是此时要注意,如果Broker端返回的SendStatus不是SEND_OK,也并不表示消息就⼀定不会推送给下游的消费者。仅仅只是表示Broker端并没有完全正确的处理这些消息。因此,如果要重新发送消息,最好要带上唯⼀的系统标识,这样在消费者端,才能⾃⾏做幂等判断。也就是⽤具有业务含义的OrderID这样的字段来判断消息有没有被重复处理。

同步的可能会导致发送耗时比较长,影响后续逻辑。

2.1.4 异步发送

异步发送机制下,⽣产者在向Broker发送消息时,会同时注册⼀个回调函数。接下来⽣产者并不等待Broker的响应。当Broker端有响应数据过来时,⾃动触发回调函数进⾏对应的处理。

//  异步发送(回调确认)
producer.send(message, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        // 发送成功回调
    }
    
    @Override
    public void onException(Throwable e) {
        // 发送失败回调
    }
});

这个回调函数是用来处理成功与失败的逻辑,如果失败了,就会执行 onException代码逻辑,可以在这里定义对应的补救措施。

注:触发了SendCallback的onException⽅法同样并不⼀定就表示消息不会向消费者推

送。如果Broker端返回响应信息太慢,超过了超时时间,也会触发 onException 方法。

超时默认是 3 秒,通过 producer.setSendMsgTimeout 可以修改。

2.2 消费者端状态确认机制

消费者端采用状态确认机制保证消费者⼀定能正常处理对应的消息。消费者的确认机制核心是 消费偏移量(Offset)的提交 ——Offset 表示消费者消费到队列的哪个位置,Broker 通过 Offset 判断消息是否已被消费。也就是消费者会返回结果给 Broker,Broker等待消费者返回消息处理状态。

2.2.1 自动确认

原理:消费线程处理消息时如果没有抛出异常,客户端会自动向 Broker 提交 Offset,Broker 标记消息已消费;如果抛出异常,不提交 Offset,Broker 会重试该消息。

注意:批量消费时,只要有一条消息消费失败,整个批次的 Offset 都不会提交,会重试整个批次。

代码:

// PushConsumer - 自动确认(默认)
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group");
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
    // 业务处理
    for (MessageExt msg : msgs) {
        System.out.println(new String(msg.getBody()));
    }
    // 自动返回CONSUME_SUCCESS
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
2.2.2 手动确认

适用于需要精准控制确认时机的场景(如消息消费后需写入数据库,需确保数据库写入成功再确认)。

// 手动确认示例
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(
        List<MessageExt> msgs, 
        ConsumeConcurrentlyContext context
    ) {
        try {
            // 业务处理
            processMessages(msgs);
            // 手动确认成功
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        } catch (Exception e) {
            // 返回失败,稍后重试
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
});
2.2.3 确认消息的关键

这个返回值是⼀个枚举值,有两个选项 CONSUME_SUCCESS 和 RECONSUME_LATER。如果消费者返回 CONSUME_SUCCESS,那么消息自然就处理结束了。但是如果消费者没有处理成功,返回的是 RECONSUME_LATER,Broker 就会过⼀段时间再发起消息重试。

消费状态如下:

  • CONSUME_SUCCESS: 消费成功,消息将被删除
  • RECONSUME_LATER: 消费失败,稍后重试

总的来说就是如下几点

  • 重试机制:未确认的消息会进入重试队列(%RETRY%+消费组名),默认重试 16 次,超过次数进入死信队列(%DLQ%+消费组名);
  • Offset 存储:Broker 将消费组 - 队列的 Offset 存储在内置主题consumerOffse中,即使消费者重启也能恢复消费进度;
  • 顺序消费确认:顺序消费默认是 “手动确认变种”,通过加锁机制保证消费顺序,只有当前消息确认后才会消费下一条。

2.3 消费者端自行指定消费点

Broker 端通过 Consumer 返回的状态来推进所属消费者组对应的 Offset。RocketMQ 可以让消费者自行定义,通过设置 ConsumeFromWhere 与 ConsumeTimestamp 来设置消费起始位置。

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group");

// 方法1:从指定时间点开始消费
consumer.setConsumeTimestamp("20260101000000"); // 格式:yyyyMMddHHmmss

// 方法2:指定消费起始位置策略
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); // 从最早开始
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);   // 从最新开始
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP);    // 从指定时间开始

ConsumeFromWhere:消费位置策略枚举

public enum ConsumeFromWhere {
    CONSUME_FROM_LAST_OFFSET,      // 从队列的第⼀条消息开始重新消费
    CONSUME_FROM_FIRST_OFFSET,     // 从上次消费到的地⽅开始继续消费
    CONSUME_FROM_TIMESTAMP         // 从某⼀个时间点开始重新消费
}

如果通过时间点来指定消费,需要指定时间点,这个时间点的格式:yyyyMMddHHmmss

3 广播消息

在 rocketmq 中,主要有两种消息的消费模式,一种是集群消费,另一种是广播消费。

  1. 集群消费(Clustering):默认模式。同一条消息只会被同一个消费者组(Consumer Group) 中的一个消费者消费。其设计目标是实现负载均衡和高可用,多个消费者共同分担消息处理任务。
  2. 广播消费(Broadcasting):同一条消息会被同一个消费者组中的每一个消费者消费一次。其设计目标是让多个订阅者都接收到完整的消息流。

集群模式下,⼀个消息,只会被⼀个消费者组中的多个消费者实例共同处理⼀次。⼴播模式下,⼀个消息,则会推送给所有消费者实例处理,不再关⼼消费者组。

3.1 简单代码

JavaAPI 代码

// !!!关键设置:将消费模式设置为广播模式 !!!
consumer.setMessageModel(MessageModel.BROADCASTING);

消息模型枚举有两种,分别是集群和广播

public enum MessageModel {
    BROADCASTING("BROADCASTING"),
    CLUSTERING("CLUSTERING");
    // ....
}

实现思路

默认模式(也就是集群模式)下,Broker端会给每个ConsumerGroup维护⼀个统⼀的Offset,这样,当Consumer来拉取消息时,就可以通过Offset保证⼀个消息,在同⼀个ConsumerGroup内只会被消费⼀次。⽽⼴播模式的本质,是将Offset转移到Consumer端⾃⾏保管,包括Offset的记录以及更新,全都放到客户端。这样Broker推送消息时,就不再管ConsumerGroup,只要Consumer来拉取消息,就返回对应的消息。

3.2 工作原理

假设你有一个消费者组 MyConsumerGroup,它下面有三个消费者实例 C1C2C3,它们都订阅了主题 MyTopic

  • 在集群模式下:主题 MyTopic 下的10条消息会以负载均衡的方式(默认为轮询)平均分配给 C1C2C3 消费。每个消息只会被其中一个实例消费。
  • 在广播模式下:主题 MyTopic 下的每一条消息,都会完整地发送给 C1C2C3 各一份。每个消费者实例都会独立消费这10条消息。
集群模式 (负载均衡)        广播模式 (全量复制)
  Msg1 -> [C1]                Msg1 -> [C1]
  Msg2 -> [C2]                Msg2 -> [C1]
  Msg3 -> [C3]      VS        Msg3 -> [C1]
  Msg4 -> [C1]                ... (以此类推,每条消息)
                              Msg1 -> [C2]
                              Msg2 -> [C2]
                              ...
                              Msg1 -> [C3]
                              Msg2 -> [C3]
                              ...

3.3 注意事项

  1. 消费进度(Offset)管理
    • 集群模式:消费进度由Broker端集中存储和管理(新版),所有消费者实例共享同一份进度。如果 C1 消费了 Msg1,整个组就认为 Msg1 已被消费。
    • 广播模式消费进度存储在消费者实例本地(例如本地文件系统)。每个实例独立维护自己的消费进度,互不影响。C1 消费到哪里,与 C2C3 完全无关。这意味着如果 C1 实例重启,它会从自己本地的记录开始消费,而不会从其他实例的进度继续。
  2. 资源消耗与性能
    • 广播模式下,网络流量和Broker的负载会成倍增加(与消费者实例数量成正比)。假设有N个实例,每条消息理论上会被传输N次。
    • 每个消费者实例都要处理全量消息,对下游业务系统(如数据库)可能造成重复写入压力,需要业务方自己处理幂等性。
  3. 适用场景
    • 缓存刷新/同步:多个应用服务器实例需要同时刷新本地缓存(如商品信息变更、配置更新)。
    • 事件通知:一个事件需要触发所有微服务实例的某个动作(如日志清理、连接重置)。
    • 数据镜像:多个异地系统需要维护一份相同的数据副本。
  4. 不适用场景
    • 需要负载分担处理大量消息的场景(如订单处理、日志收集)。此时应使用集群模式。
    • 对消息处理有严格的“只处理一次”要求,且不希望业务层做复杂幂等处理的场景。

4 过滤消息

RocketMQ 的过滤消息功能允许消费者只接收其感兴趣的消息,而不是订阅主题下的所有消息。这在多种消费者对同一主题有不同关注点的场景下非常有用。

当我们相同的模块的消息都放到同一个 Topic 中,但是不同的业务需要根据不同的需求去获取不同的消息。例如仓储系统,入库出库消息都是在仓储的 Topic 中,仓储系统需要记账,就根据所需的消息去做进账与出账业务。

RocketMQ 的消息过滤是在 Broker 端执行的核心机制,核心是通过Tag 标签过滤和SQL92 属性过滤两种方式,让消费者只接收符合条件的消息,减少无效网络传输与消费端压力,过滤在服务端完成,遵循 “匹配即投递” 原则。

4.1 Tag 过滤

基于消息的 Tag 标签进行过滤,Tag 是消息的子分类标识,每个消息只能有一个 Tag。

语法规则

  • 单 Tag 匹配:如 "CREATE",只接收标签为 CREATE 的消息。
  • 多 Tag 匹配:多个 Tag 间用 || 分隔(逻辑或),如 "CREATE||PAY||CANCEL"。(不支持模糊匹配)
  • 全匹配:用 * 表示接收该主题下所有消息。

实现原理

  1. 生产者发送消息时,Tag 会被计算哈希值存入 ConsumeQueue(消息消费逻辑队列)。
  2. Broker 过滤时先通过 ConsumeQueue 中的 Tag 哈希值快速匹配,命中后再校验消息原始 Tag 字符串,避免哈希冲突导致误匹配。
  3. 消费端拉取到消息后,也会二次校验 Tag 原始值,确保过滤准确性。

代码案例

生产者,向 Topic 是 TopicTest 发送 TagA、TagB、TagC 三种标签的消息。

public class TagFilterProducer {

    public static final int MESSAGE_COUNT = 20;
    public static final String PRODUCER_GROUP = "Study1";
    public static final String DEFAULT_NAMESRVADDR = "192.168.109.134:9876";
    public static final String TOPIC = "TopicTest";
    public static final List<String> TAGS = List.of("TagA", "TagB", "TagC");

    public static void main(String[] args) throws MQClientException, InterruptedException {
        // 创建消费者,指定消费组
        DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
        // 设置Nameserver地址
        producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
        // 启动生产者
        producer.start();
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            try {
                // 创建消息体:订阅主题、Tag、消息体
                Message msg = new Message(TOPIC,
                        TAGS.get(i % TAGS.size()),
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
                );
                // 发送消息
                SendResult sendResult = producer.send(msg, 20 * 1000);
                System.out.printf("%s%n", sendResult);
            } catch (Exception e) {
                e.printStackTrace();
                Thread.sleep(1000);
            }
        }
        // 关闭生产者,释放资源
        producer.shutdown();
    }
}

过滤消费者

通过指定 Tag 规则进行过滤。

public class TagFilterConsumer {
    public static final String CONSUMER_GROUP = "Study1";
    public static final String DEFAULT_NAMESRVADDR = "192.168.109.134:9876";
    public static final String TOPIC = "TopicTest";

    public static void main(String[] args) throws MQClientException {
        // 创建消费者,指定消费组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
        // 设置Nameserver地址
        consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
        // 如果特定的消费群体是全新的,请指定从哪里开始
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        // 配置订阅主题Topic、Tag
//        consumer.subscribe(TOPIC, "*");
//        consumer.subscribe(TOPIC, "TagA");
        consumer.subscribe(TOPIC, "TagA || TagB");
        // 设置回调函数,处理消息
        consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
            System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        // 启动消费者,启动后会一直挂着,持续处理消息
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

过滤可以获取全部的消息、指定单个的 Tag 或者指定多个 Tag。

// 通配
consumer.subscribe(TOPIC, "*");
// 单个
consumer.subscribe(TOPIC, "TagA");
// 多个
consumer.subscribe(TOPIC, "TagA || TagB");

适用场景:简单的消息分类过滤,如订单创建、支付、取消等不同类型消息的分离,适合高吞吐量场景,性能损耗低。

4.2 SQL 过滤

SQL 过滤是基于消息的自定义属性(key-value)进行过滤,支持 SQL92 语法的子集,可实现复杂条件匹配,每个消息可设置多个属性

支持语法元素

  • 比较运算符:=<>>>=<<=
  • 逻辑运算符:ANDORNOT
  • 常量:字符串(用单引号)、数字、NULL
  • 函数:INLIKEBETWEEN 等。
  • 模糊匹配:% 匹配任意 0 个或多个字符, _ 匹配任意单个字符 。

实现原理:依赖 rocketmq-filter 模块构建并执行 SQL 表达式,过滤时需读取消息的完整属性进行计算,性能略低于 Tag 过滤,但灵活性更高。

案例代码

生产者还是指定标签 ABC,并且可以自定义写入属性信息。

public class SQLFilterProducer {
    public static final int MESSAGE_COUNT = 20;
    public static final String PRODUCER_GROUP = "Study1";
    public static final String DEFAULT_NAMESRVADDR = "192.168.109.134:9876";
    public static final String TOPIC = "SQLTopic";
    public static final List<String> TAGS = List.of("TagA", "TagB", "TagC");

    public static void main(String[] args) throws MQClientException, InterruptedException {
        // 创建消费者,指定消费组
        DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP);
        // 设置Nameserver地址
        producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
        // 启动生产者
        producer.start();
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            try {
                // 创建消息体:订阅主题、Tag、消息体
                Message msg = new Message(TOPIC,
                        TAGS.get(i % TAGS.size()),
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
                );
                // 生产者发送带自定义属性的消息
                msg.putUserProperty("orderId", "123456");
                msg.putUserProperty("qty", String.valueOf(10 + i));
                // 发送消息
                SendResult sendResult = producer.send(msg, 20 * 1000);
                System.out.printf("%s%n", sendResult);
            } catch (Exception e) {
                e.printStackTrace();
                Thread.sleep(1000);
            }
        }
        // 关闭生产者,释放资源
        producer.shutdown();
    }
}

消费者通过 SQL 进行过滤

public class SQLFilterConsumer {
    public static final String CONSUMER_GROUP = "Study1";
    public static final String DEFAULT_NAMESRVADDR = "192.168.109.134:9876";
    public static final String TOPIC = "SQLTopic";

    public static void main(String[] args) throws MQClientException {
        // 创建消费者,指定消费组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
        // 设置Nameserver地址
        consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
        // 如果特定的消费群体是全新的,请指定从哪里开始
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        // 配置订阅主题Topic、SQL
        consumer.subscribe(TOPIC,
                MessageSelector.bySql("(TAGS IS NOT NULL AND TAGS IN ('TagA', 'TagB)) " +
                        "AND (orderId IS NOT NULL AND qty > 15)"));
        // 设置回调函数,处理消息
        consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
            System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        // 启动消费者,启动后会一直挂着,持续处理消息
        consumer.start();
        System.out.printf("Consumer Started.%n");
    }
}

过滤出标签是 TagA 和 TagB 以及 qty 大于 15 并且 orderId 不能为空。

适用场景:复杂业务过滤,如根据订单金额、时间范围、用户等级等多维度筛选消息。

注:通过 TAGS 过滤是不用担心性能问题,但是通过自定义属性就会造成性能问题。 RocketMQ 中被过滤掉的消息不会消失,过滤只是限制这些消息投递给当前匹配过滤条件的消费者,消息本身的生命周期完全由 Broker 的存储策略决定,和是否被过滤没有关系。

由于模糊匹配(特别是 %xxx 这种前置通配符)会导致 Broker 无法利用索引,必须逐条扫描消息属性,在消息量巨大时会严重影响性能。如果可能,建议在发送消息时将需要模糊匹配的维度拆分为独立的消息属性(例如将 orderType 拆分为 prefix 和 suffix 两个属性),或者直接使用 Tag 进行粗粒度过滤,再在消费端进行细粒度的模糊匹配。

5 顺序消息

RocketMQ 的顺序消息(FIFO 消息)是一种**高级消息类型,支持消费者按照发送消息的先后顺序获取消息,从而实现业务场景中的顺序处理。它通过消息组 (MessageGroup)** 机制保证局部顺序,在满足业务顺序需求的同时兼顾系统吞吐能力。

生产端:保证发送顺序

  • 队列选择器:通过MessageQueueSelector将同一业务标识(如订单 ID)的消息路由到同一个队列,避免跨队列乱序
  • 同步发送:必须使用同步发送send()),禁止异步发送,确保消息发送成功后再发送下一条
  • 消息组机制:新版 RocketMQ 通过MessageGroup属性标记消息归属,服务端自动路由到对应队列。

⽣产者只有将⼀批有顺序要求的消息,放到同⼀个MesasgeQueue上,通过MessageQueue的FIFO特性保证这⼀批消息的顺序。如果不指定MessageSelector对象,那么⽣产者会采⽤轮询的⽅式将多条消息依次发送到不同的MessageQueue上。

生产者案例代码

// 生产者发送顺序消息
producer.send(message, new MessageQueueSelector() {
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        // 按订单ID哈希选择队列,确保同一订单消息进入同一队列
        Integer orderId = (Integer) arg;
        int index = orderId % mqs.size();
        return mqs.get(index);
    }
}, orderId); // 传递订单ID作为路由参数

消费端:保证处理顺序

  • 顺序消费模式:使用MessageListenerOrderly(而非并发模式MessageListenerConcurrently
  • 队列独占:同一队列同一时间仅分配给一个消费线程,线程按消息顺序串行处理
  • 队列锁机制:消费端通过ConsumeOrderlyContext自动加锁,防止同一队列被多线程并发消费。

消费者需要实现MessageListenerOrderly接⼝,实际上在服务端,处理MessageListenerOrderly时,会给⼀个MessageQueue加锁,拿到 MessageQueue上所有的消息,然后再去读取下⼀个MessageQueue的消息。

消费者案例代码

consumer.registerMessageListener(new MessageListenerOrderly() {
    AtomicLong consumeTimes = new AtomicLong(0);
    @Override
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
        context.setAutoCommit(true);
        System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
        this.consumeTimes.incrementAndGet();
        if ((this.consumeTimes.get() % 2) == 0) {
            return ConsumeOrderlyStatus.SUCCESS;
        } else if ((this.consumeTimes.get() % 5) == 0) {
            context.setSuspendCurrentQueueTimeMillis(3000);
            return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
        }

        return ConsumeOrderlyStatus.SUCCESS;
    }
});

原理就是将消息都发到同一个MessageQueue。具体可以看官网:顺序消息 | RocketMQ

注:

1、一般场景下是局部有序,如果需要全局有序,那就将消息统一放到一个 MessageQueue 。

2、⽣产者端尽可能将同一组有序消息打散到不同的 MessageQueue 上,避免过于集中导致数据热点竞争。

3、消费者端只进⾏有限次数的重试。如果⼀条消息处理失败,RocketMQ会将后续消息阻塞住,让消费者进⾏重试。但是,如果消费者⼀直处理失败,超过最⼤重试次数,那么RocketMQ就会跳过这⼀条消息,处理后⾯的消息,这会造成消息乱序。

4、消费者端如果确实处理逻辑中出现问题,不建议抛出异常,可以返回 ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT作为替代。

6 延迟消息

RocketMQ 的延迟消息是一种高级消息特性,指消息发送到 Broker 后,不会立即被消费者消费,而是在指定时间(或延迟级别对应的时间)后才投递到目标 Topic 供消费者处理。该机制广泛应用于订单超时处理、定时任务调度、通知提醒等场景。

6.1 原理

消息发送后,在固定的时间后才被消费,官方有两个版本,两者本质相同,均通过服务端定时调度实现,5.x 版本统一称为定时消息。

  • RocketMQ 4.x 默认支持18 个固定延迟级别,不支持任意时间延迟。

Broker 配置可自定义延迟级别:messageDelayLevel=1s 5s 10s 30s 1m ... 1h 2h

  • RocketMQ 5.x 引入精确定时消息,支持毫秒级任意时间延迟,通过设置绝对时间戳实现。

6.2 代码案例

生产者的核心代码

Message message = new Message(TOPIC, ("Hello scheduled message " + i).getBytes(StandardCharsets.UTF_8));
// This message will be delivered to consumer 10 seconds later.
message.setDelayTimeLevel(3);
// Send the message
SendResult result = producer.send(message);
System.out.print(result);

通过设置延迟时间级别 message.setDelayTimeLevel(3)代表 10 秒后发送。Broker 接收消息后,判断为延迟消息,将其暂存到内部主题 SCHEDULE_TOPIC_XXXX。Broker 启动定时任务线程池,每个延迟级别对应一个定时任务,定时任务周期性扫描对应队列,判断消息是否到期(存储时间 + 延迟时间≤当前时间),到期消息被重新投递到原始目标 Topic 的对应队列,消费者此时可正常消费该消息。

Message message = new Message(TOPIC, ("Hello scheduled message " + i).getBytes(StandardCharsets.UTF_8));
// This message will be delivered to consumer 10 seconds later.
//message.setDelayTimeSec(10);
// The effect is the same as the above
// message.setDelayTimeMs(10_000L);
// Set the specific delivery time, and the effect is the same as the above
// long deliverTime = LocalDateTime.of(2026, 1, 20, 10, 0, 0)
//     .atZone(ZoneId.systemDefault())
//     .toInstant()
//     .toEpochMilli();
// message.setDeliverTimeMs(deliverTime);
message.setDeliverTimeMs(System.currentTimeMillis() + 10_000L);
// Send the message
SendResult result = producer.send(message);
System.out.printf(result + "\n");

时间轮将时间划分为固定槽位,每个槽位对应一个时间间隔。消息根据到期时间放入对应槽位,通过指针循环扫描处理到期消息。具体可以看官网:延迟消息发送 | RocketMQ

7 批量消息

在对吞吐率有一定要求的情况下,Apache RocketMQ可以将一些消息聚成一批以后进行发送,可以增加吞吐率,并减少API和网络调用次数。

案例代码

// 添加批量消息
List<Message> messages = new ArrayList<>(MESSAGE_COUNT);
for (int i = 0; i < MESSAGE_COUNT; i++) {
    messages.add(new Message(TOPIC, TAG, "OrderID" + i, ("Hello world " + i).getBytes(StandardCharsets.UTF_8)));
}

// 消息过多需要分片
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
    List<Message> listItem = splitter.next();
    SendResult sendResult = producer.send(listItem);
    System.out.printf("%s", sendResult);
}

注:

批量消息的使用非常简单,但是要注意RocketMQ做了限制。同一批消息的Topic必须相同,另外,不支持延迟消息。

还有批量消息的大小不要超过1M,如果太大就需要自行分割。

8 事务消息

事务消息为 Apache RocketMQ 中的高级特性消息,本文为您介绍事务消息的应用场景、功能原理、使用限制、使用方法和使用建议。

事务消息是 Apache RocketMQ 提供的一种高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。

8.1 工作流程

简单的描述以上内容(后续会有文章来单独介绍,具体内容请看原文:事务消息 | RocketMQ

首先,生产者将消息发送至Apache RocketMQ服务端,实际上是向 Broker(Server)发送一条 “半消息”(Half Message),Broker 确认接收成功后,这条消息暂时不会投递给消费者。然后生产者开始执行本地业务事务(比如数据库操作),确保业务逻辑和消息发送的一致性。接着本地事务执行完成后,生产者向 Broker 发送 Commit(事务成功)或 Rollback(事务失败)指令,告知 Broker 最终处理结果。事务状态回查有补偿机制,如果 Broker 长时间未收到生产者的指令,会主动触发事务回查,向生产者询问本地事务的最终状态;生产者重新检查本地事务并反馈结果。最后 Broker 收到 Commit 指令时,会将半消息正式投递给消费者(Subscriber);收到 Rollback 指令时,则直接丢弃半消息,不进行投递。

8.2 生命周期

  • 初始化:半事务消息被生产者构建并完成初始化,待发送到服务端的状态。
  • 事务待提交:半事务消息被发送到服务端,和普通消息不同,并不会直接被服务端持久化,而是会被单独存储到事务存储系统中,等待第二阶段本地事务返回执行结果后再提交。此时消息对下游消费者不可见。
  • 消息回滚:第二阶段如果事务执行结果明确为回滚,服务端会将半事务消息回滚,该事务消息流程终止。
  • 提交待消费:第二阶段如果事务执行结果明确为提交,服务端会将半事务消息重新存储到普通存储系统中,此时消息对下游消费者可见,等待被消费者获取并消费。
  • 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,Apache RocketMQ会对消息进行重试处理。具体信息。
  • 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。 Apache RocketMQ默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。
  • 消息删除:Apache RocketMQ按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。

8.3 官方实例代码

① 实现 TransactionListener 接口

public class TransactionListenerImpl implements TransactionListener {
    // 原子整型:生成自增数值,模拟不同的本地事务结果(线程安全,适配多线程发送场景)
    private AtomicInteger transactionIndex = new AtomicInteger(0);
    // 并发哈希表:存储「事务消息ID-事务状态」的映射,供回查时使用(线程安全)
    private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();

    // 执行本地事务
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        int value = transactionIndex.getAndIncrement();
        int status = value % 3;
        localTrans.put(msg.getTransactionId(), status);
        return LocalTransactionState.UNKNOW;
    }

    // 回查事务状态
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        Integer status = localTrans.get(msg.getTransactionId());
        if (null != status) {
            switch (status) {
                case 0:
                    return LocalTransactionState.UNKNOW;
                case 1:
                    return LocalTransactionState.COMMIT_MESSAGE;
                case 2:
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                default:
                    return LocalTransactionState.COMMIT_MESSAGE;
            }
        }
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}

② 生产者

public class TransactionProducer {

    public static final String PRODUCER_GROUP = "please_rename_unique_group_name";
    public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
    public static final String TOPIC = "TopicTest1234";

    public static final int MESSAGE_COUNT = 10;

    public static void main(String[] args) throws MQClientException, InterruptedException {
        TransactionListener transactionListener = new TransactionListenerImpl();
        TransactionMQProducer producer = new TransactionMQProducer(PRODUCER_GROUP);

        producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
        ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000), r -> {
            Thread thread = new Thread(r);
            thread.setName("client-transaction-msg-check-thread");
            return thread;
        });

        producer.setExecutorService(executorService);
        producer.setTransactionListener(transactionListener);
        producer.start();

        String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            try {
                Message msg =
                    new Message(TOPIC, tags[i % tags.length], "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.sendMessageInTransaction(msg, null);
                System.out.printf("%s%n", sendResult);

                Thread.sleep(10);
            } catch (MQClientException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }

        // 保持程序运行(关键!否则生产者关闭后无法接收回查回调)
        for (int i = 0; i < 100000; i++) {
            Thread.sleep(1000);
        }
        producer.shutdown();
    }
}

这两段代码,其中 TransactionListenerImpl 是事务监听器的实现类(负责处理本地事务和事务回查),TransactionProducer 是事务消息生产者(负责发送事务消息并驱动整个流程)。

public enum LocalTransactionState {
    COMMIT_MESSAGE, // “提交”,消息对消费者可见;
    ROLLBACK_MESSAGE, // “回滚”,消息被丢弃;
    UNKNOW; //  “未知”,RocketMQ 会继续回查(直到超时);
}

executeLocalTransaction 返回 UNKNOW,或生产者与 RocketMQ 通信异常时,RocketMQ会定时回调该方法,确认本地事务最终状态;

必须使用 TransactionMQProducer来发送消息。

9 ACL 权限控制机制

访问控制 2.0(ACL 2.0)是Apache RocketMQ的访问控制列表(Access Control List)升级版本,提供了完善的身份认证(Authentication)和权限授权(Authorization)机制,用于保护RocketMQ集群的数据安全。

我们可以在控制界面给 topic 设置权限

如图中的 perm 表示 topic 的权限,有三个可选项。 2:禁写禁订阅,4:可订阅,不能写,6:可写可订阅。

在Broker端还提供了更详细的权限控制机制。主要是在broker.conf中打开acl的标志:aclEnable=true。就可以用plain_acl.yml来进⾏权限配置了,并且是热加载,无需重启。

但是我这个是 2.0,在 ACL2.0 这种方法已经被移除了(后续文章将详细描写)。

ACL 2.0 可以通过配置 broker.conf 来设置认证授权等。

总结

RocketMQ 客户端消息机制具备清晰的使用逻辑与完善的可靠性保障:生产者、消费者均有固定编程步骤;消息确认机制分生产端(同步 / 异步 / 单向发送的差异化确认,关联 Broker 刷盘 / 复制策略)和消费端(基于 Offset 的自动 / 手动确认,含重试、死信队列等兜底);消费模式支持集群(负载均衡,Broker 管理 Offset)与广播(全量消费,本地维护 Offset);消息过滤可通过 Tag(简单高效)或 SQL92(复杂灵活)在 Broker 端实现,减少无效传输。


转发请携带作者信息  @怒放吧德德 @一个有梦有戏的人
持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!
👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍
谢谢支持!