消息队列篇——RocketMQ

RocketMQ 使用详解:从入门到实战

消息中间件系列文章 · 第二弹 ------ 继 RabbitMQ 之后,带你全面掌握 RocketMQ 的架构、使用与实战

目录


一、RocketMQ 概述

1.1 什么是 RocketMQ

RocketMQ 是阿里巴巴开源的分布式消息中间件,基于 Java 语言开发,2016 年捐赠给 Apache 软件基金会,2017 年成为 Apache 顶级项目。它是一款低延迟、高吞吐、高可用、强一致的分布式消息队列,被广泛应用于电商、金融、物流、社交等大规模分布式系统中。

RocketMQ 名字的由来:Rocket(火箭)+ MQ(Message Queue,消息队列),寓意"像火箭一样快的消息队列"。

1.2 RocketMQ 能做什么

消息队列的核心作用是解耦、异步、削峰填谷,RocketMQ 在此基础上还提供了更多高级能力:

① 核心作用(所有消息队列通用):

作用 说明 示例
解耦 生产者与消费者解耦,互不依赖 订单系统只管发消息,库存/积分/短信各自消费
异步 耗时操作异步化,快速响应 用户注册后异步发送邮件/短信
削峰填谷 高峰时缓冲请求,匀速消费 秒杀场景:10万请求 → 队列 → 按 1000/s 消费

② RocketMQ 的独立特性:

能力 说明 典型场景
顺序消息 保证消息的全局/局部有序 订单状态流转、日志按序入库
事务消息 分布式事务最终一致性方案 下单 + 扣库存 + 发消息的一致性
延迟消息 消息指定时间后投递 订单超时未支付自动关闭
消息轨迹 追踪消息从生产到消费的全过程 消息丢失排查、链路追踪

1.3 主流消息队列对比

特性 RocketMQ RabbitMQ Kafka 说明
开发语言 Java Erlang Scala/Java ---
协议 自研协议 AMQP 自研协议 RocketMQ 支持 HTTP/自定义协议
吞吐量 高(10万级/秒) 中(万级/秒) 极高(百万级/秒) 单机吞吐
延迟 毫秒级 微秒级 毫秒级 RabbitMQ 延迟最低
顺序消息 支持(分区强顺序) 支持(需单队列) 支持(分区内有序) 均为局部有序
事务消息 支持(半消息机制) 有限支持(事务/发布确认) 支持(事务性 API) 实现机制不同
延迟消息 支持(18 个级别) 支持(TTL+死信/插件) 不支持(需自研) RocketMQ 内置延迟级别
消息回溯 支持(按时间/偏移量) 有限支持 支持(按 Offset) 重放历史消息
死信队列 支持(原生) 支持(原生 DLX) 不支持(需自研) ---
消息轨迹 支持(内置) RocketMQ 原生支持
管理界面 支持(Dashboard) 支持(Management) 支持(第三方工具) ---
社区活跃度 高(阿里主导) 极高 ---

选型建议:

  • 需要顺序消息 + 事务消息 + 延迟消息 ,Java 技术栈 → 选 RocketMQ
  • 追求极低延迟、复杂路由规则、简单场景 → 选 RabbitMQ
  • 追求极高吞吐、日志/流处理场景 → 选 Kafka

1.4 RocketMQ 版本演进

版本 发布时间 核心特性
3.x 2016 开源初版,双主双从
4.0 2017 成为 Apache 顶级项目,事务消息
4.5 2019 DLedger 多副本、ACL、消息轨迹
4.9 2021 大量性能优化、JFR 支持
5.0 2022 云原生架构升级、Pop 消费、轻量代理、gRPC 协议
5.1+ 2023+ 火箭 5.0 生态完善,Controller 模式(新主从架构)

本文章以 RocketMQ 4.9.x(经典稳定版) 为主,兼顾 5.x 新特性 介绍。


二、核心概念与架构

2.1 架构全景图

RocketMQ 由 四大核心组件 组成:

复制代码
┌─────────────────────────────────────────────────────────────┐
│                          Producer                             │
│                        (消息生产者)                          │
└──────────────────────────┬──────────────────────────────────┘
                           │ 1. 发送消息
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                        NameServer                            │
│                     (注册中心 / 路由中心)                    │
│   • 管理 Broker 的注册与发现      • 路由信息管理               │
│   • 轻量级,无状态,可多节点部署   • 各节点间不通信             │
└───────────────┬──────────────────────────┬───────────────────┘
                │ 2. 注册路由信息           │ 3. 获取路由信息
                ▼                          ▼
┌─────────────────────────────────────────────────────────────┐
│                         Broker                               │
│                    (消息服务器 / 存储节点)                   │
│   • Topic 消息存储        • 消息消费管理                      │
│   • 高可用(主从)        • 支持多种刷盘策略                   │
└──────────────────────────┬──────────────────────────────────┘
                           │ 4. 拉取/推送消息
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                         Consumer                             │
│                     (消息消费者)                            │
└─────────────────────────────────────────────────────────────┘

完整消息流转流程:

复制代码
① Producer 启动 → ② 从 NameServer 获取 Broker 路由信息
③ Producer 连接 Broker 发送消息 → ④ Broker 存储消息并返回结果
⑤ Consumer 启动 → ⑥ 从 NameServer 获取路由信息
⑦ Consumer 连接 Broker 拉取消息 → ⑧ 消费完成上报消费位点

2.2 核心组件详解

(1)NameServer ------ 注册中心
  • 职责:管理 Broker 的路由信息(Topic 与 Broker 的映射关系),是"无状态"的轻量级节点
  • 特点
    • 各 NameServer 节点之间互不通信,数据不完全一致(弱一致性)
    • 每个 NameServer 保存全量路由信息
    • Producer/Consumer 启动时从任意一个 NameServer 获取路由信息
    • 支持热部署,宕机不影响已建立的连接(但有 120s 感知延迟)
  • 部署建议:至少 2 台,避免单点
(2)Broker ------ 消息服务器
  • 职责 :消息的存储与转发核心节点
  • 结构 :Broker 分为 Master(主)Slave(从) 两种角色
    • Master:负责读写消息
    • Slave:从 Master 同步数据,提供读服务(消费)容灾
  • 存储机制 :消息采用顺序写 + 零拷贝(mmap + page cache)的方式写入 CommitLog 文件,保证了极高的写入性能
  • 高可用 :4.5 之前靠主从同步,4.5+ 引入 DLedger(基于 Raft) 实现自动选主
(3)Producer ------ 生产者
  • 负责生产消息,通过轮询/故障转移等策略选择队列发送
  • 消息发送方式:同步发送、异步发送、单向发送
(4)Consumer ------ 消费者
  • 负责消费消息,支持两种消费模式:集群消费(默认)、广播消费
  • 支持两种消费类型:
    • Push 模式:服务端主动推送(实际上底层是长轮询拉取)
    • Pull 模式:客户端主动拉取

2.3 核心概念详解

概念 说明 类比(RabbitMQ)
Topic 消息的一级分类,消息存储的逻辑载体 Exchange + Queue 合并概念
Tag 消息的二级分类,用于 Topic 内细粒度过滤 Routing Key / Binding
Message 消息实体,包含 Topic、Tag、Body、Key、属性 Message
Message Queue 每个 Topic 下划分为多个队列(默认 4 个),是消息存储的最小逻辑单元 Queue(一个 Topic 多 Queue)
Group 生产者组 / 消费者组,一组同类生产/消费者的集合 ---
ConsumerGroup 消费者组:组内消费者共同消费一个 Topic,一条消息只被组内一个消费者消费(集群模式) 消费组
Offset 消费位点,记录消费者消费到队列的哪个位置 Offset
CommitLog 消息的物理存储文件,所有 Topic 的消息顺序写入 ---
ConsumeQueue 逻辑队列,基于 CommitLog 构建的索引,每个队列一个 ---
IndexFile 索引文件,按 Key 或时间戳查询消息 ---

2.4 Topic 与 Message Queue 的关系

复制代码
                       Topic: OrderTopic
                              │
        ┌────────────┬────────┼────────┬────────────┐
        ▼            ▼        ▼        ▼            ▼
   Queue 0       Queue 1    Queue 2   Queue 3  ...  Queue N(默认4个)
        │            │        │        │            │
        ▼            ▼        ▼        ▼            ▼
   ConsumeQueue  ConsumeQueue ConsumeQueue ...
        │            │        │        │
        └────────────┴────────┴────────┴────────────┘
                      全部指向同一个
                    CommitLog 物理文件

关键设计

  • 一个 Topic 的所有消息物理上都顺序存储在同一个 CommitLog 中
  • 每个 Queue 通过 ConsumeQueue(逻辑索引)定位自己在 CommitLog 中的消息
  • 并行度:一个 Topic 的吞吐上限 ≈ 队列数 × 单队列吞吐,队列数决定了消费并行度
  • 顺序性:同一条消息路由到同一个 Queue 才能保证顺序(见第六章)

2.5 消息存储模型

复制代码
RocketMQ 存储目录结构(默认 store 目录)
├── commitlog/                 # 消息物理存储文件,每个文件 1GB
│   ├── 00000000000000000000   # 文件名为起始偏移量
│   └── 00000000001073741824
├── consumequeue/              # 逻辑消费队列
│   └── {Topic}
│       └── {QueueId}
│           ├── 00000000000000000000
│           └── ...
├── index/                     # 索引文件
│   └── 20260813150000000
├── config/                    # 配置(Topic/消费组等)
│   ├── topics.json
│   ├── consumerOffset.json
│   └── subscriptionGroup.json
└── abort                     # 上次是否正常关闭的标志文件

写入流程(顺序写 + 零拷贝):

复制代码
Producer 发送消息
    │
    ▼
Broker 收到消息
    │
    ▼
写入 CommitLog(顺序追加写,通过 mmap 映射到 page cache)
    │
    ▼
异步构建 ConsumeQueue 索引
    │
    ▼
根据刷盘策略(同步刷盘/异步刷盘)持久化到磁盘
    │
    ▼
返回发送结果给 Producer

为什么快? 所有 Topic 的消息共享一个 CommitLog 文件顺序追加写,避免了随机 IO;读时通过 ConsumeQueue 索引定位,配合 page cache 与零拷贝(sendfile)技术,实现高吞吐。


三、安装部署

3.1 环境要求

项目 要求
操作系统 Linux / macOS / Windows(生产推荐 Linux)
JDK 8+(推荐 8/11/17)
内存 至少 4GB(默认配置 JVM 内存较大,可按需调整)
磁盘 SSD 优先(消息存储 IO 密集)

3.2 方式一:Docker 一键部署(推荐)

bash 复制代码
# 1. 创建数据目录
mkdir -p /data/rocketmq/{namesrv,broker,logs}

# 2. 启动 NameServer
docker run -d --name rmqnamesrv \
  -p 9876:9876 \
  -v /data/rocketmq/logs:/home/rocketmq/logs \
  apache/rocketmq:4.9.4 sh mqnamesrv

# 3. 创建 Broker 配置文件 broker.conf
cat > /data/rocketmq/broker.conf <<'EOF'
brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=0
# 本机内网 IP(重要!客户端要能访问到)
brokerIP1=192.168.1.100
# 自动创建 Topic 开关
autoCreateTopicEnable=true
autoCreateSubscriptionGroup=true
# 存储路径
storePathRootDir=/home/rocketmq/store
# 刷盘方式:ASYNC_FLUSH 异步 / SYNC_FLUSH 同步
flushDiskType=ASYNC_FLUSH
# 主从同步方式:ASYNC_MASTER / SYNC_MASTER
brokerRole=ASYNC_MASTER
EOF

# 4. 启动 Broker
docker run -d --name rmqbroker \
  -p 10911:10911 -p 10909:10909 -p 10912:10912 \
  -v /data/rocketmq/broker.conf:/home/rocketmq/rocketmq-4.9.4/conf/broker.conf \
  -v /data/rocketmq/logs:/home/rocketmq/logs \
  -v /data/rocketmq/store:/home/rocketmq/store \
  apache/rocketmq:4.9.4 sh mqbroker \
  -n 192.168.1.100:9876 \
  -c /home/rocketmq/rocketmq-4.9.4/conf/broker.conf

# 5. 验证
docker exec rmqnamesrv sh mqadmin clusterList -n localhost:9876

3.3 方式二:Linux 二进制安装

bash 复制代码
# 1. 下载解压(以 4.9.4 为例)
wget https://archive.apache.org/dist/rocketmq/4.9.4/rocketmq-all-4.9.4-bin-release.zip
unzip rocketmq-all-4.9.4-bin-release.zip
cd rocketmq-4.9.4

# 2. 调整 JVM 内存(默认 4G/8G 太大,开发机建议调小)
#    修改 bin/runserver.sh(NameServer):
#    JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m"
#    修改 bin/runbroker.sh(Broker):
#    JAVA_OPT="${JAVA_OPT} -server -Xms512m -Xmx512m -Xmn256m"

# 3. 启动 NameServer
nohup sh bin/mqnamesrv > namesrv.log 2>&1 &
tail -f namesrv.log    # 看到 "The Name Server boot success" 即成功

# 4. 启动 Broker(-n 指定 NameServer 地址)
nohup sh bin/mqbroker -n localhost:9876 \
  -c conf/broker.conf > broker.log 2>&1 &
tail -f broker.log     # 看到 "The broker boot success" 即成功

# 5. 测试发送/接收
export NAMESRV_ADDR=localhost:9876
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer

3.4 方式三:RocketMQ Dashboard(可视化控制台)

bash 复制代码
# Docker 方式
docker run -d --name rocketmq-dashboard \
  -p 8080:8080 \
  -e JAVA_OPTS="-Drocketmq.namesrv.addr=192.168.1.100:9876" \
  apacherocketmq/rocketmq-dashboard:latest

# 访问 http://localhost:8080 即可打开控制台

控制台常用功能:Topic 管理、消费组管理、消息查询(按 Topic/Key/时间)、消息轨迹、集群状态、Broker 配置。

3.5 端口说明

端口 用途
9876 NameServer 端口(客户端/ Broker 连接)
10911 Broker 主端口(客户端连接,FastRemoting)
10909 VIP 通道端口(10911 - 2)
10912 Broker HA 主从同步端口

注意 :生产环境务必关闭 Broker 的 VIP 通道brokerIP1brokerIP2 不一致时触发),否则客户端可能连不上。可在 broker.conf 中设置 vipChannelEnabled=false 或确保端口放行。


四、快速入门:第一个生产者与消费者

4.1 引入依赖

xml 复制代码
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.9.4</version>
</dependency>

4.2 生产者:发送消息

java 复制代码
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;

public class QuickStartProducer {

    public static void main(String[] args) throws Exception {
        // 1. 创建生产者,指定生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("quick_start_producer_group");

        // 2. 设置 NameServer 地址(多个用分号分隔)
        producer.setNamesrvAddr("localhost:9876");

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

        try {
            // 4. 循环发送 10 条消息
            for (int i = 0; i < 10; i++) {
                // 创建消息:Topic + Tag + 消息体(byte[])
                Message msg = new Message(
                        "QuickStartTopic",       // Topic
                        "TagA",                  // Tag,用于二级过滤
                        ("Hello RocketMQ " + i).getBytes()  // 消息体
                );
                // 可选:设置消息 Key(用于查询和去重)
                msg.setKeys("order_20260813_" + i);

                // 5. 同步发送,并获取发送结果
                org.apache.rocketmq.client.producer.SendResult result =
                        producer.send(msg);

                System.out.printf("发送成功: msgId=%s, queueId=%d, offset=%d%n",
                        result.getMsgId(),
                        result.getMessageQueue().getQueueId(),
                        result.getOffset());
            }
        } finally {
            // 6. 关闭生产者(释放连接)
            producer.shutdown();
        }
    }
}

预期输出:

复制代码
发送成功: msgId=C0A8016400002A9F0000000000000000, queueId=3, offset=0
发送成功: msgId=C0A8016400002A9F0000000000000001, queueId=0, offset=0
发送成功: msgId=C0A8016400002A9F0000000000000002, queueId=1, offset=0
...

4.3 消费者:接收消息

java 复制代码
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.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

public class QuickStartConsumer {

    public static void main(String[] args) throws Exception {
        // 1. 创建消费者,指定消费者组名
        DefaultMQPushConsumer consumer =
                new DefaultMQPushConsumer("quick_start_consumer_group");

        // 2. 设置 NameServer 地址
        consumer.setNamesrvAddr("localhost:9876");

        // 3. 设置消费起始位置(首次消费时生效)
        //    CONSUME_FROM_LAST_OFFSET:从最新消息开始(默认)
        //    CONSUME_FROM_FIRST_OFFSET:从最早消息开始
        //    CONSUME_FROM_TIMESTAMP:从指定时间点开始
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

        // 4. 订阅 Topic 和 Tag(* 表示全部 Tag)
        consumer.subscribe("QuickStartTopic", "*");
        // 精确订阅多个 Tag:consumer.subscribe("QuickStartTopic", "TagA || TagB");

        // 5. 注册消息监听器(并发消费)
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(
                    List<MessageExt> msgs, ConsumeConcurrentlyContext context) {

                for (MessageExt msg : msgs) {
                    System.out.printf("收到消息: %s, Tag=%s, Key=%s, body=%s%n",
                            new String(msg.getBody()),
                            msg.getTags(),
                            msg.getKeys(),
                            msg.getMsgId());
                }
                // 返回消费成功状态
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        // 6. 启动消费者
        consumer.start();
        System.out.println("Consumer Started.");

        // 7. 保持运行(简单演示)
        Thread.sleep(60_000);
        consumer.shutdown();
    }
}

4.4 三种发送方式对比

方式 API 可靠性 延迟 适用场景
同步发送 producer.send(msg) 高(有结果返回) 重要消息、需要确认结果的场景
异步发送 producer.send(msg, callback) 高(回调确认) 追求吞吐、批量场景
单向发送 producer.sendOneway(msg) 低(不确认) 最低 日志等可丢失消息

异步发送示例:

java 复制代码
// 异步发送:不阻塞主线程,通过回调处理结果
producer.send(msg, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        System.out.println("异步发送成功: " + sendResult.getMsgId());
    }

    @Override
    public void onException(Throwable e) {
        System.out.println("异步发送失败: " + e.getMessage());
        // 这里可以记录日志,或重试
    }
});

单向发送示例:

java 复制代码
// 单向发送:只管发,不关心结果,性能最高
producer.sendOneway(msg);

五、普通消息详解

5.1 消息结构

RocketMQ 的 Message 核心字段:

字段 说明
topic 消息主题(必填)
tags 消息标签,用于过滤(可选)
keys 业务 Key,用于消息查询/去重(可选)
body 消息体,byte\[\](必填)
waitStoreMsgOK 是否等待消息存储完成(默认 true)
delayTimeLevel 延迟级别(见第七章)
flag 标志位
properties 扩展属性(用户自定义)

消息属性(properties)自定义:

java 复制代码
Message msg = new Message("OrderTopic", "TagA", body);
// 自定义属性
msg.putUserProperty("orderId", "10086");
msg.putUserProperty("userId", "9527");
msg.putUserProperty("source", "app");

// 消费端读取自定义属性
String orderId = msg.getUserProperty("orderId");

5.2 Tag 过滤机制

Tag 是消息的二级分类,消费端订阅时可指定 Tag 实现服务端过滤

复制代码
生产者发送:Topic=OrderTopic, Tag=TagA
             Topic=OrderTopic, Tag=TagB
             Topic=OrderTopic, Tag=TagC

消费者1订阅:OrderTopic, Tag=TagA   → 只收到 TagA 的消息
消费者2订阅:OrderTopic, Tag=TagA || TagB → 收到 TagA 和 TagB
消费者3订阅:OrderTopic, *         → 收到全部消息

注意 :Tag 过滤是服务端过滤 (Broker 根据 ConsumeQueue 中的 Tag 哈希预过滤),效率高。但同一个消息只能有一个 Tag,如果需要多维度分类,可配合自定义属性在消费端二次过滤。

5.3 批量发送

RocketMQ 支持一次性批量发送消息(需配置 producer.setRetryTimesWhenSendFailed):

java 复制代码
// 批量发送:提高吞吐
List<Message> msgs = new ArrayList<>();
for (int i = 0; i < 100; i++) {
    Message msg = new Message("BatchTopic", "TagA",
            ("batch message " + i).getBytes());
    msgs.add(msg);
}
// 批量发送,一次发送的 batch 不超过 4MB
producer.send(msgs);

批量发送限制:一次发送的消息总大小不能超过 4MB,且每条消息不能超过 4MB。

5.4 发送重试机制

java 复制代码
DefaultMQProducer producer = new DefaultMQProducer("group");
// 同步发送失败重试次数(默认 2)
producer.setRetryTimesWhenSendFailed(3);
// 异步发送失败重试次数(默认 2)
producer.setRetryTimesWhenSendAsyncFailed(3);
// 超时时间,默认 3000ms
producer.setSendMsgTimeout(5000);

5.5 消息 Key 与业务去重

设置 Key 的价值:

  1. 快速查询:在控制台/管理端按 Key 检索消息
  2. 幂等去重:消费端可用 Key + 业务表唯一索引实现幂等
java 复制代码
// 生产者:以业务主键作为 Key
Message msg = new Message("OrderTopic", "TagA", body);
msg.setKeys(orderId);   // 如订单号

// 消费端幂等方案(Redis 去重)
String key = msg.getKeys();
boolean first = redis.setIfAbsent("MQ:IDEMPOTENT:" + key, "1", 24, TimeUnit.HOURS);
if (first) {
    // 首次消费,执行业务逻辑
    processOrder(msg);
}
// 否则直接返回成功(已消费过)

六、顺序消息

6.1 什么是顺序消息

顺序消息 指消息的消费顺序与生产顺序保持一致。RocketMQ 提供分区有序(局部有序):

  • 全局顺序:一个 Topic 只有一个 Queue,所有消息按序消费(吞吐低)

  • 分区顺序 :一个 Topic 多个 Queue,同一业务键的消息路由到同一 Queue,按序消费(推荐)

    无序(普通消息):
    Producer ── 轮询 ──> Queue0 ──┐
    Queue1 ──┼──> 消费顺序随机
    Queue2 ──┘

    有序(顺序消息,按业务键路由):
    订单 1001 的所有消息 ──取模──> Queue2(固定)──> 按序消费
    订单 1002 的所有消息 ──取模──> Queue0(固定)──> 按序消费

6.2 顺序消息实现:MessageQueueSelector

核心原理 :发送时通过 MessageQueueSelector 自定义队列选择策略,让同一业务键(如订单号)的消息 总是进入同一个队列 ,消费端使用 MessageListenerOrderly 顺序消费。

java 复制代码
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.common.message.MessageQueue;

public class OrderProducer {

    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("order_producer_group");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();

        // 模拟一个订单的状态流转(4 条消息)
        String orderId = "ORDER_10086";
        String[] states = {"已创建", "已支付", "已发货", "已完成"};

        for (int i = 0; i < states.length; i++) {
            Message msg = new Message("OrderSeqTopic", "TagA",
                    ("订单 " + orderId + " 状态变更为:" + states[i]).getBytes());

            // 关键:使用 MessageQueueSelector 按业务键选择队列
            SendResult result = producer.send(msg, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    // arg 即传入的 orderId,对队列数取模,同一订单固定同一队列
                    int index = Math.abs(((String) arg).hashCode()) % mqs.size();
                    return mqs.get(index);
                }
            }, orderId);   // 第三个参数 arg = orderId

            System.out.printf("发送成功: queueId=%d, %s%n",
                    result.getMessageQueue().getQueueId(),
                    new String(msg.getBody()));
        }

        producer.shutdown();
    }
}

6.3 顺序消费者

java 复制代码
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;

public class OrderConsumer {

    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer =
                new DefaultMQPushConsumer("order_consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("OrderSeqTopic", "*");

        // 关键:使用 MessageListenerOrderly(顺序消费)
        // 该监听器内部使用单线程消费同一个队列的消息
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(
                    List<MessageExt> msgs, ConsumeOrderlyContext context) {

                // 顺序消费默认会加分布式锁(Broker 锁 + 客户端锁)
                context.setAutoCommit(true);  // 自动提交消费进度

                for (MessageExt msg : msgs) {
                    System.out.printf("顺序消费: %s%n", new String(msg.getBody()));
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

        consumer.start();
        System.out.println("Order Consumer Started.");
    }
}

6.4 顺序消息注意事项

注意点 说明
消费失败 顺序消息重试会阻塞当前队列,直到成功(避免乱序)
消息积压 某条消息失败会阻塞其后续消息,需监控消费进度
锁机制 顺序消费依赖 Broker 分布式锁 + 客户端本地锁,保证同一队列同时只有一个消费者消费
扩容 增加消费者时,队列重新分配可能导致短暂的重平衡
全局顺序 设置 Topic 队列数为 1,但吞吐受限,一般不建议

七、延迟消息

7.1 什么是延迟消息

延迟消息 :消息发送后,不立即投递给消费者,而是等待指定的延迟时间后才投递。

典型场景:

  • 订单下单后 30 分钟未支付,自动取消
  • 定时任务延迟执行(如 5 分钟后重试)
  • 活动预热通知(提前 1 小时通知用户)

7.2 延迟级别(DelayTimeLevel)

RocketMQ 内置了 18 个固定延迟级别,不能随意指定秒数:

Level 延迟时间 Level 延迟时间
1 1s 10 6min
2 5s 11 7min
3 10s 12 8min
4 30s 13 9min
5 1min 14 10min
6 2min 15 20min
7 3min 16 30min
8 4min 17 1h
9 5min 18 2h

延迟时间在 messageDelayLevel 配置中定义,可修改(改后需重启 Broker)。

7.3 使用示例

java 复制代码
public class DelayProducer {

    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("delay_producer_group");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();

        Message msg = new Message("DelayTopic", "TagA",
                "订单 ORDER_8888 30秒后未支付将自动取消".getBytes());

        // 设置延迟级别:5 表示 1 分钟(见级别表)
        msg.setDelayTimeLevel(5);

        SendResult result = producer.send(msg);
        System.out.println("延迟消息发送成功: " + result.getMsgId());
        System.out.println("消息将在 1 分钟后投递");

        producer.shutdown();
    }
}

消费端与普通消息完全一样,无需任何特殊配置,只是投递时间被延后。

7.4 实现原理

复制代码
┌──────────┐   发送    ┌──────────────────────────────────┐
│ Producer │──────────▶│          Broker                   │
└──────────┘           │                                  │
                       │ ① 延迟消息不直接写入消息 Topic 队列 │
                       │ ② 写入 "SCHEDULE_TOPIC_XXXX" 定时队列 │
                       │ ③ 定时任务扫描到期消息             │
                       │ ④ 到期后写入真实 Topic 的队列      │
                       │ ⑤ 消费者正常消费                  │
                       └──────────────────────────────────┘

7.5 实现"任意延迟时间"的方案

内置级别有限,如果需要任意秒级延迟,常用方案:

方案 原理 优缺点
定时任务 + 普通消息 自己维护延时任务表,定时扫描发送 简单但增加复杂度
升级自定义级别 修改 messageDelayLevel 配置 只能改一次,需重启
时间轮(5.x Timer 消息) RocketMQ 5.0 支持任意延迟 需 5.x + Controller 模式

八、事务消息

8.1 为什么需要事务消息

经典问题:本地事务与消息发送的一致性

复制代码
场景:用户下单 → 写入订单表(DB) + 发送扣库存消息(MQ)

方案一:先写 DB,再发消息
    DB 成功、消息发送失败 → 库存没扣 → 不一致 ❌

方案二:先发消息,再写 DB
    消息发送成功、DB 失败 → 库存扣了但订单不存在 → 不一致 ❌

RocketMQ 事务消息 通过「半消息(Half Message)+ 消息回查 」机制解决该问题,实现分布式事务的最终一致性

8.2 事务消息原理

复制代码
┌─────────┐   ① 发送半消息      ┌─────────┐
│ Producer │ ──────────────────▶ │ Broker  │
└─────────┘                      └─────────┘
     │                                │
     │ ② 执行本地事务(写订单表)       │
     ▼                                │
┌─────────┐                           │
│    DB   │                           │
└─────────┘                           │
     │                                │
     │ ③ 提交/回滚本地事务结果          │
     ▼                                │
┌─────────┐   ④ commit/rollback      │
│ Producer │ ───────────────────────▶│ 消息可见/不可见
└─────────┘                           │
     │ ⑤ 如果 Producer 挂了/超时        │
     └────────── ⑥ 回查(Check)────────▶ 询问本地事务状态
                 (Broker 主动回查)     │ 直到拿到明确结果
                                       ▼

核心流程:

  1. 发送半消息:Producer 发送消息,Broker 存储但不投递(对消费者不可见)
  2. 执行本地事务:Producer 收到半消息成功后,执行本地事务(如写订单表)
  3. 提交/回滚 :本地事务执行完成后,Producer 向 Broker 提交(Commit)或回滚(Rollback)
    • Commit → 消息对消费者可见,正常投递
    • Rollback → 消息被删除,不投递
  4. 消息回查 :如果 Producer 在步骤 3 之前宕机(本地事务执行了但没来得及提交),Broker 会定时回查本地事务状态,直到得到明确结果(最多回查 15 次)

8.3 事务消息代码实现

第一步:实现事务监听器 TransactionListener

java 复制代码
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;

import java.util.concurrent.ConcurrentHashMap;

public class OrderTransactionListener implements TransactionListener {

    // 模拟本地事务执行结果缓存(key = 事务ID)
    private final ConcurrentHashMap<String, Boolean> localTrans =
            new ConcurrentHashMap<>();

    /**
     * 执行本地事务:返回本地事务的执行状态
     * 这是半消息发送成功后由 RocketMQ 回调的方法
     */
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        String transactionId = msg.getTransactionId();
        System.out.println("【步骤2】执行本地事务,transactionId=" + transactionId);

        try {
            // 模拟:执行本地事务(如 INSERT 订单表)
            boolean success = saveOrderToDB(msg);

            if (success) {
                // 本地事务成功 → 提交消息
                localTrans.put(transactionId, true);
                return LocalTransactionState.COMMIT_MESSAGE;
            } else {
                // 本地事务失败 → 回滚消息
                localTrans.put(transactionId, false);
                return LocalTransactionState.ROLLBACK_MESSAGE;
            }
        } catch (Exception e) {
            // 异常:状态未知,等待回查
            e.printStackTrace();
            localTrans.put(transactionId, null);
            return LocalTransactionState.UNKNOW;
        }
    }

    /**
     * 回查本地事务:Broker 发现状态未知时回调
     * (Producer 在提交前宕机、或返回 UNKNOW 时触发)
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        String transactionId = msg.getTransactionId();
        System.out.println("【步骤5】Broker 回查本地事务,transactionId=" + transactionId);

        // 查询本地事务表,确定事务最终状态
        Boolean result = localTrans.get(transactionId);
        if (Boolean.TRUE.equals(result)) {
            return LocalTransactionState.COMMIT_MESSAGE;
        } else if (Boolean.FALSE.equals(result)) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
        // 还没结果,返回 UNKNOW 继续等待下次回查
        return LocalTransactionState.UNKNOW;
    }

    /** 模拟写订单表 */
    private boolean saveOrderToDB(Message msg) throws Exception {
        String body = new String(msg.getBody());
        System.out.println("写入订单表: " + body);
        // 模拟 80% 概率成功
        return Math.random() > 0.2;
    }
}

第二步:使用事务消息生产者

java 复制代码
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionSendResult;

import java.util.concurrent.*;

public class TransactionProducer {

    public static void main(String[] args) throws Exception {
        // 使用 TransactionMQProducer(事务生产者)
        TransactionMQProducer producer = new TransactionMQProducer("tx_producer_group");
        producer.setNamesrvAddr("localhost:9876");

        // 设置事务监听器
        producer.setTransactionListener(new OrderTransactionListener());

        // 可选:设置事务回查线程池
        ExecutorService executor = 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(executor);

        producer.start();

        // 发送事务消息(与普通消息 API 不同!)
        Message msg = new Message("TxOrderTopic", "TagA",
                ("订单 ORDER_6666 创建").getBytes());
        msg.setKeys("ORDER_6666");

        TransactionSendResult result = producer.sendMessageInTransaction(msg, null);
        System.out.println("事务消息发送结果: " + result.getLocalTransactionState());

        producer.shutdown();
    }
}

8.4 事务消息使用规范

规范 说明
不支持延迟消息 事务消息不能设置延迟级别
不支持批量消息 事务消息只能单条发送
回查次数 Broker 默认回查 15 次,每次间隔按 transactionCheckInterval(默认 60s)递增
消息可见性 半消息对消费者不可见,Commit 后才可见
消费端幂等 事务消息可能重复投递,消费端仍需幂等处理
适用场景 订单 + 库存、账户 + 积分、支付 + 通知等跨系统一致性场景

九、批量消息

9.1 批量发送

java 复制代码
public class BatchProducer {

    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("batch_producer_group");
        producer.setNamesrvAddr("localhost:9876");
        // 批量发送失败重试次数(默认 2)
        producer.setRetryTimesWhenSendFailed(3);
        producer.start();

        // 构造批量消息
        List<Message> messages = new ArrayList<>();
        for (int i = 0; i < 50; i++) {
            messages.add(new Message("BatchTopic", "TagA",
                    ("批量消息 " + i).getBytes()));
        }

        // 一次批量发送(总大小不超过 4MB)
        SendResult result = producer.send(messages);
        System.out.println("批量发送成功: " + result.getMsgId());
        System.out.println("消息总数: " + messages.size());

        producer.shutdown();
    }
}

9.2 分批发送工具(超过 4MB 时)

java 复制代码
public class BatchMessageSplitter {

    /** 按大小分批:每批不超过 4MB */
    public static List<List<Message>> split(List<Message> messages) {
        List<List<Message>> batches = new ArrayList<>();
        List<Message> current = new ArrayList<>();
        int currentSize = 0;

        for (Message msg : messages) {
            // 每条消息大小 = body + topic + properties 等
            int msgSize = msg.getTopic().length() + msg.getBody().length;
            if (currentSize + msgSize > 4 * 1024 * 1024) {  // 4MB
                batches.add(current);
                current = new ArrayList<>();
                currentSize = 0;
            }
            current.add(msg);
            currentSize += msgSize;
        }
        if (!current.isEmpty()) {
            batches.add(current);
        }
        return batches;
    }

    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("batch_producer_group");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();

        List<Message> all = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            all.add(new Message("BatchTopic", "TagA",
                    ("消息内容 " + i).getBytes()));
        }

        // 分批发送
        for (List<Message> batch : split(all)) {
            producer.send(batch);
        }
        System.out.println("分批发送完成,共 " + all.size() + " 条消息");

        producer.shutdown();
    }
}

9.3 批量消费

java 复制代码
// RocketMQ 的 push 消费默认就是批量拉取的(每个 batch 默认最多 32 条)
consumer.setConsumeMessageBatchMaxSize(16);  // 单次回调最大消息数(默认 1)
// 注意:批量消费时,监听器收到的 List 可能包含多条消息

注意consumeMessageBatchMaxSize 只是拉取批量 的提示,实际消息数由 Broker 按积压量决定。消费端应逐条处理 List 中的消息。


十、消费模式:集群消费与广播消费

10.1 两种消费模式对比

特性 集群消费(Clustering) 广播消费(Broadcasting)
默认模式 ✅ 默认 ❌ 需配置
消息分配 一条消息只被组内一个消费者消费 一条消息被组内所有消费者消费
消费进度 Broker 端存储(集群共享) 消费者本地存储
适用场景 负载均衡、并行处理 每个节点都需要全量消息
重平衡 支持动态重平衡 无重平衡
使用方式 consumer.setMessageModel(MessageModel.CLUSTERING) consumer.setMessageModel(MessageModel.BROADCASTING)
复制代码
集群消费:
┌─────────┐   ┌─────────┐   ┌─────────┐
│Consumer1│   │Consumer2│   │Consumer3│
│ (组A)   │   │ (组A)   │   │ (组A)   │
└────┬────┘   └────┬────┘   └────┬────┘
     └─────────────┼─────────────┘
                    ▼
            消息被轮流消费(负载均衡)
            每条消息只被消费一次

广播消费:
┌─────────┐   ┌─────────┐   ┌─────────┐
│Consumer1│   │Consumer2│   │Consumer3│
│ (组A)   │   │ (组A)   │   │ (组A)   │
└────┬────┘   └────┬────┘   └────┬────┘
     │            │            │
     └───── 每条消息都被所有消费者消费 ─────┘

10.2 集群消费代码

java 复制代码
public class ClusterConsumer {

    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer =
                new DefaultMQPushConsumer("cluster_consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("ClusterTopic", "*");

        // 集群消费(默认就是,可省略,但建议显式声明)
        consumer.setMessageModel(MessageModel.CLUSTERING);

        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                System.out.println("集群消费: " + new String(msg.getBody()));
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        consumer.start();
        System.out.println("Cluster Consumer Started.");
    }
}

部署 2 个以上同组消费者实例,即可看到消息被平均分配消费(重平衡机制)。

10.3 广播消费代码

java 复制代码
public class BroadcastConsumer {

    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer =
                new DefaultMQPushConsumer("broadcast_consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("BroadcastTopic", "*");

        // 切换为广播消费(关键!)
        consumer.setMessageModel(MessageModel.BROADCASTING);

        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                System.out.println("广播消费: " + new String(msg.getBody()));
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        consumer.start();
        System.out.println("Broadcast Consumer Started.");
    }
}

广播消费注意 :消费进度存在本地$HOME/.rocketmq_offsets),服务重启后从头开始可能导致重复消费,需自行做好幂等。

10.4 消费进度与位点(Offset)

复制代码
消息队列:Queue0
消息位点:[msg0@offset0] [msg1@offset1] [msg2@offset2] [msg3@offset3] ...
                    ▲                                ▲
             消费位点(已消费)                最大位点(最新消息)

消费进度 = min(所有消费者的消费位点)

消费起始位置三种策略:

java 复制代码
// 1. 从最新开始(默认,只消费新消息)
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);

// 2. 从最早开始(消费全部历史消息)
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

// 3. 从指定时间点开始
consumer.setConsumeTimestamp("20260813150000");  // 格式:yyyyMMddHHmmss
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP);

三种策略只在消费者组第一次消费(无历史位点)时生效,后续消费以已存储的 Offset 为准。

10.5 重置消费位点

需要重新消费历史消息时(如修复线上 bug 后重放):

bash 复制代码
# 方式一:命令行重置(回退到最早)
sh bin/mqadmin resetOffsetByTime \
  -n localhost:9876 \
  -g consumer_group \
  -t TopicName \
  -s 0        # 时间戳,0 = 最早

# 方式二:控制台重置
# RocketMQ Dashboard → 消费组 → 重置消费位点 → 选择时间

十一、消息可靠性:ACK 与重试机制

11.1 消息全链路可靠性

复制代码
┌─────────────────────────────────────────────────────────┐
│             消息全链路可靠性保障                          │
├─────────────────────────────────────────────────────────┤
│ ① 生产阶段:同步发送 + 发送重试 + 事务消息                │
│    保证:消息不丢(已写入 Broker)                       │
├─────────────────────────────────────────────────────────┤
│ ② 存储阶段:同步刷盘 + 主从同步(SYNC_MASTER)            │
│    保证:Broker 宕机消息不丢                            │
├─────────────────────────────────────────────────────────┤
│ ③ 消费阶段:消费重试 + 死信队列 + 手动 ACK                │
│    保证:消息最终被消费(至少一次)                       │
└─────────────────────────────────────────────────────────┘

11.2 消费端 ACK 机制

RocketMQ 的 Push 消费是"自动 ACK"模式,通过返回状态告知 Broker 处理结果:

java 复制代码
// 消费成功 → 告诉 Broker 消息已处理完,可以移除
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

// 消费失败 → 消息会进入重试队列,稍后重新投递
return ConsumeConcurrentlyStatus.RECONSUME_LATER;

消费失败重试流程:

复制代码
消费失败(返回 RECONSUME_LATER)
    │
    ▼
进入重试队列 %RETRY%consumerGroup
    │
    ▼
延迟重试(延迟级别递增:1s → 5s → 10s → ... → 2h)
    │
    ▼
重试 16 次后仍然失败
    │
    ▼
进入死信队列 %DLQ%consumerGroup

11.3 消费重试配置

java 复制代码
DefaultMQPushConsumer consumer =
        new DefaultMQPushConsumer("retry_consumer_group");

// 1. 消费失败重试次数(默认 16 次,范围 1~16)
consumer.setMaxReconsumeTimes(5);

// 2. 单条消息消费超时时间(默认 15 分钟)
consumer.setConsumeTimeout(10 * 60 * 1000);  // 10 分钟

// 3. 并发消费线程数(默认 20)
consumer.setConsumeThreadMin(5);
consumer.setConsumeThreadMax(20);

// 4. 拉取消息批量大小(默认 32)
consumer.setPullBatchSize(32);

11.4 重试间隔与级别

RocketMQ 消费重试采用延迟级别递增策略(与延迟消息共用 18 级):

重试次数 延迟时间 重试次数 延迟时间
第 1 次 1s 第 9 次 5min
第 2 次 5s 第 10 次 6min
第 3 次 10s 第 11 次 7min
第 4 次 30s 第 12 次 8min
第 5 次 1min 第 13 次 9min
第 6 次 2min 第 14 次 10min
第 7 次 3min 第 15 次 20min
第 8 次 4min 第 16 次 30min

11.5 顺序消费的失败重试

java 复制代码
// 顺序消费:失败后原地阻塞重试(不跳过后面的消息)
consumer.registerMessageListener(new MessageListenerOrderly() {
    @Override
    public ConsumeOrderlyStatus consumeMessage(
            List<MessageExt> msgs, ConsumeOrderlyContext context) {

        try {
            // 业务处理
            process(msgs);
            return ConsumeOrderlyStatus.SUCCESS;
        } catch (Exception e) {
            // 暂停当前队列消费(挂起重试)
            context.setSuspendCurrentQueueTimeMillis(3000);  // 3 秒后重试
            return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
        }
    }
});

11.6 消费幂等(重点)

RocketMQ 保证消息至少一次(At Least Once)投递,消费端必须做幂等处理

幂等方案 实现方式 适用场景
数据库唯一索引 以业务主键建唯一索引,插入冲突则忽略 订单、流水类
Redis SetNX 消费前 SETNX key value,成功才处理 高频去重
状态机校验 消费时校验业务状态是否已变更 状态流转类
去重表 单独建消息去重表记录 msgId 通用方案
java 复制代码
// 数据库唯一索引幂等示例(MySQL)
// 表:t_order (order_id VARCHAR PRIMARY KEY, ...)
public void handleOrderMessage(MessageExt msg) {
    String orderId = msg.getUserProperty("orderId");
    try {
        // INSERT ... 主键冲突说明已处理过
        orderMapper.insert(parseOrder(msg));
    } catch (DuplicateKeyException e) {
        // 重复消费,忽略
        log.warn("重复消息,忽略: orderId={}", orderId);
    }
}

十二、死信队列

12.1 什么是死信队列

死信队列(Dead Letter Queue, DLQ):消息重试次数用尽仍消费失败,则该消息进入死信队列。

复制代码
正常消息流:
Topic → Consumer 消费成功 → 完成 ✅

重试流程:
Topic → 消费失败 → %RETRY%group(重试队列)→ 重试 N 次
                                              │
                                    16 次仍失败 ↓
                                        %DLQ%group(死信队列)

12.2 死信队列特征

特征 说明
命名规则 %DLQ%消费者组名(如 %DLQ%order_consumer_group
权限 死信队列只允许消费,不允许生产
保留时间 默认保留 72 小时(brokerDeleteExpiredMessage 控制)
消费方式 需要新的消费者组单独订阅消费
延迟 死信消息在第 4 天才会被清理(72h + 1天缓冲)

12.3 死信消息查看

bash 复制代码
# 1. 查看死信队列
sh bin/mqadmin topicList -n localhost:9876 | grep DLQ

# 2. 查看死信消息(按 Topic 查询)
sh bin/mqadmin queryMsgByKey \
  -n localhost:9876 \
  -t %DLQ%order_consumer_group \
  -k 订单号

# 控制台方式:RocketMQ Dashboard → 消息查询 → 死信队列 Topic

12.4 死信消息处理(补偿)

java 复制代码
// 使用【新的消费者组】订阅死信队列,进行人工/自动补偿
public class DLQConsumer {

    public static void main(String[] args) throws Exception {
        // 注意:消费者组名不能与原来相同
        DefaultMQPushConsumer consumer =
                new DefaultMQPushConsumer("dlq_handler_group");

        consumer.setNamesrvAddr("localhost:9876");
        // 订阅死信队列(%DLQ% + 原消费者组名)
        consumer.subscribe("%DLQ%order_consumer_group", "*");

        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                System.out.println("收到死信消息: " + new String(msg.getBody()));
                System.out.println("原始 Topic: " + msg.getTopic());
                System.out.println("重试次数: " + msg.getReconsumeTimes());

                // 死信补偿策略:
                // 1. 记录告警(发短信/邮件通知人工处理)
                alert(msg);
                // 2. 或修复数据后重新发送到正常 Topic
                resendToNormalTopic(msg);
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        consumer.start();
        System.out.println("DLQ Consumer Started.");
    }

    private static void alert(MessageExt msg) {
        System.out.println("【告警】消息处理失败多次: " + msg.getMsgId());
    }

    private static void resendToNormalTopic(MessageExt msg) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("repair_producer");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();

        // 重新发送到原 Topic,由原消费者组再次消费
        Message newMsg = new Message(
                "OrderTopic", msg.getTags(),
                msg.getKeys(), msg.getBody());
        producer.send(newMsg);
        System.out.println("死信消息已重新投递");
        producer.shutdown();
    }
}

12.5 死信处理最佳实践

复制代码
死信消息处理流程(推荐):
① 监控死信队列(数量 > 0 即告警)
② 查询死信消息内容,定位失败原因
③ 修复数据/依赖(如补库存、重启依赖服务)
④ 重新投递(重新发送到原 Topic)或人工处理
⑤ 处理完成后记录补偿日志,闭环

十三、刷盘策略与主从复制

13.1 刷盘策略(Flush Disk)

Broker 收到消息后,先写入 page cache(内存),再由 OS 刷入磁盘。两种刷盘策略:

策略 配置值 可靠性 吞吐 说明
异步刷盘 ASYNC_FLUSH 中(可能丢消息) 写入 page cache 即返回成功,由 OS 定期刷盘
同步刷盘 SYNC_FLUSH 高(不丢消息) 消息落盘后才返回成功
复制代码
异步刷盘:
Producer → Broker 内存(page cache) → 返回成功 ✅ → OS 异步刷盘
                  ↑ 宕机时可能丢失未落盘数据

同步刷盘:
Producer → Broker 内存 → 刷盘成功 → 返回成功 ✅
                  ↑ 数据已落盘,宕机不丢失

配置方式(broker.conf):

properties 复制代码
# 异步刷盘(默认,高吞吐)
flushDiskType=ASYNC_FLUSH

# 同步刷盘(高可靠)
flushDiskType=SYNC_FLUSH

13.2 主从复制(Broker 高可用)

模式 配置值 可靠性 说明
异步复制 ASYNC_MASTER Master 写成功即返回,Slave 异步同步
同步双写 SYNC_MASTER Master 同步到 Slave 后才返回成功
复制代码
异步复制(ASYNC_MASTER):
Producer → Master 写入成功 → 返回 ✅ → 异步同步到 Slave
                                      (Master 宕机可能丢最近消息)

同步双写(SYNC_MASTER):
Producer → Master 写入 → 同步到 Slave → 双方成功 → 返回 ✅
                                      (Master 宕机不丢消息)

配置示例(4.x 经典主从):

properties 复制代码
# broker-a.conf(Master)
brokerClusterName=DefaultCluster
brokerName=broker-a
brokerId=0                    # 0 表示 Master
brokerIP1=192.168.1.100
flushDiskType=ASYNC_FLUSH
brokerRole=ASYNC_MASTER       # 或 SYNC_MASTER
deleteWhen=04
fileReservedTime=72

# broker-a-s.conf(Slave)
brokerClusterName=DefaultCluster
brokerName=broker-a           # 与 Master 同名!
brokerId=1                    # 非 0 表示 Slave
brokerIP1=192.168.1.101
flushDiskType=ASYNC_FLUSH
brokerRole=SLAVE

重要 :Master 和 Slave 的 brokerName 必须相同,brokerId 不同(Master=0,Slave>0)。

13.3 刷盘与复制组合矩阵

组合 可靠性 吞吐 适用场景
异步刷盘 + 异步复制 最低 最高 日志、统计类(可容忍少量丢失)
异步刷盘 + 同步双写 一般业务(推荐默认)
同步刷盘 + 异步复制 中高 订单类核心业务
同步刷盘 + 同步双写 最高 最低 金融、交易类强一致场景

十四、集群部署:DLedger 模式

14.1 为什么要 DLedger

4.x 经典主从架构(Master/Slave)的痛点:

  • 主从切换需要人工干预:Master 宕机后,Slave 只能提供读,不能自动升级为 Master
  • 存在脑裂风险:网络分区时可能同时有两个 Master

DLedger(Distributed Ledger) 基于 Raft 一致性算法 实现自动选主,是 RocketMQ 4.5+ 的高可用方案:

复制代码
DLedger 三节点集群(Raft 协议):

┌───────────┐   ┌───────────┐   ┌───────────┐
│  Broker-0 │   │  Broker-1 │   │  Broker-2 │
│ (Leader)  │──▶│ (Follower)│   │ (Follower)│
└───────────┘   └───────────┘   └───────────┘
     │ 写请求只走 Leader           ▲
     ▼                           │ 自动选举
   所有消息同步复制到 Follower     │
                                  │ Leader 宕机
                        Follower 自动成为新 Leader

14.2 DLedger 集群配置(3 节点)

properties 复制代码
# broker-d0.conf(节点 1)
brokerClusterName=RaftCluster
brokerName=RaftNode00
brokerId=0
brokerIP1=192.168.1.101
# DLedger 配置
enableDLegerCommitLog=true
dLegerGroup=RaftNode00
dLegerPeers=n0-192.168.1.101:40911;n1-192.168.1.102:40911;n2-192.168.1.103:40911
dLegerSelfId=n0
storePathRootDir=/data/rocketmq/store
flushDiskType=ASYNC_FLUSH

# broker-d1.conf(节点 2)
brokerClusterName=RaftCluster
brokerName=RaftNode01
brokerId=0
brokerIP1=192.168.1.102
enableDLegerCommitLog=true
dLegerGroup=RaftNode01
dLegerPeers=n0-192.168.1.101:40911;n1-192.168.1.102:40911;n2-192.168.1.103:40911
dLegerSelfId=n1
storePathRootDir=/data/rocketmq/store
flushDiskType=ASYNC_FLUSH

# broker-d2.conf(节点 3,同上修改 IP 和 dLegerSelfId=n2)
bash 复制代码
# 三个节点分别启动
nohup sh bin/mqbroker -n 192.168.1.101:9876 -c broker-d0.conf &
nohup sh bin/mqbroker -n 192.168.1.102:9876 -c broker-d1.conf &
nohup sh bin/mqbroker -n 192.168.1.103:9876 -c broker-d2.conf &

# 查看集群状态
sh bin/mqadmin clusterList -n 192.168.1.101:9876

14.3 经典主从 vs DLedger 对比

特性 经典主从(4.x 传统) DLedger(4.5+)
主从切换 手动(改配置重启) 自动(Raft 选举)
一致性 异步/同步复制 Raft 多数派确认
最少节点 2(1主1从) 3(多数派需要)
数据一致性 可能丢少量消息 不丢(多数派已确认)
运维复杂度
适用场景 中小规模 对可用性要求高的大规模

14.4 5.x Controller 新架构

RocketMQ 5.x 引入 Controller 模式(基于 DLedger 改造):

复制代码
Controller 模式架构:

┌───────────────┐     ┌───────────────┐
│  Controller-1 │     │  Controller-2 │   ← Controller 集群
└───────┬───────┘     └───────┬───────┘   (管理主从选举)
        └──────────┬──────────┘
                   │
       ┌───────────┼───────────┐
       ▼           ▼           ▼
  ┌─────────┐ ┌─────────┐ ┌─────────┐
  │ Broker-A│ │ Broker-B│ │ Broker-C│   ← Broker 数据节点
  │ (Master)│ │ (Slave) │ │ (Slave) │
  └─────────┘ └─────────┘ └─────────┘

优势:Controller 与 Broker 分离,部署更灵活,支持 2 个 Controller + 2 个 Broker 的轻量高可用方案。


十五、ACL 权限控制

15.1 什么是 ACL

ACL(Access Control List,访问控制列表):RocketMQ 4.4+ 提供的权限控制机制,防止未授权客户端访问 Broker。

权限模型:

权限 说明
DENY 拒绝
PUB 只允许发送
SUB 只允许消费
PUB|SUB 允许发送和消费

15.2 开启 ACL

properties 复制代码
# broker.conf 中开启
aclEnable=true

15.3 配置授权文件 plain_acl.yml

yaml 复制代码
# 全局白名单(免认证 IP)
globalWhiteRemoteAddresses:
  - 10.10.10.*
  - 192.168.0.*

accounts:
  - accessKey: RocketMQ      # 认证密钥
    secretKey: 12345678      # 密钥
    whiteRemoteAddress:      # 账号级白名单
    admin: false             # 是否管理员
    defaultTopicPerm: DENY   # 默认 Topic 权限
    defaultGroupPerm: SUB    # 默认消费组权限
    topicPerms:              # Topic 级权限
      - topicA=PUB|SUB
      - topicB=PUB
      - topicC=SUB
    groupPerms:              # 消费组级权限
      - groupA=PUB|SUB
      - groupB=SUB

  - accessKey: admin
    secretKey: admin123
    admin: true              # 管理员:全部权限

15.4 客户端接入认证

java 复制代码
// 方式一:RPCHook 认证
import org.apache.rocketmq.acl.common.AclClientRPCHook;
import org.apache.rocketmq.acl.common.SessionCredentials;

public class AclProducer {

    public static void main(String[] args) throws Exception {
        // 创建认证 Hook(accessKey + secretKey 对应 plain_acl.yml)
        AclClientRPCHook rpcHook = new AclClientRPCHook(
                new SessionCredentials("RocketMQ", "12345678"));

        // 生产者和消费者都需要传入 Hook
        DefaultMQProducer producer = new DefaultMQProducer(
                "acl_producer_group", rpcHook);
        producer.setNamesrvAddr("localhost:9876");
        producer.start();

        Message msg = new Message("topicA", "TagA", "ACL 消息".getBytes());
        producer.send(msg);
        System.out.println("ACL 认证发送成功");

        producer.shutdown();
    }
}
java 复制代码
// 方式二:Spring 配置注入
// @Value 注入密钥,构造 Hook 传入
@Configuration
public class RocketMQConfig {

    @Value("${rocketmq.acl.accessKey}")
    private String accessKey;

    @Value("${rocketmq.acl.secretKey}")
    private String secretKey;

    private RPCHook getAclHook() {
        return new AclClientRPCHook(new SessionCredentials(accessKey, secretKey));
    }

    @Bean
    public DefaultMQProducer producer() {
        DefaultMQProducer producer =
                new DefaultMQProducer("prod_group", getAclHook());
        producer.setNamesrvAddr("localhost:9876");
        return producer;
    }
}

15.5 ACL 最佳实践

复制代码
① 关闭自动创建 Topic(autoCreateTopicEnable=false),所有 Topic 走审批
② 最小权限原则:只授权业务实际需要的 Topic/权限
③ 密钥分级:普通业务账号 + 管理员账号分离
④ 定期轮换密钥
⑤ 网络安全:ACL 只是应用层认证,还需配合防火墙/Security Group

十六、消息轨迹与消息查询

16.1 消息轨迹(Message Trace)

消息轨迹:追踪一条消息从生产、存储到消费的完整生命周期,用于问题排查。

开启方式:

properties 复制代码
# broker.conf 开启轨迹(默认关闭)
traceTopicEnable=true
java 复制代码
// 生产者开启轨迹
DefaultMQProducer producer = new DefaultMQProducer("group");
producer.setEnableMsgTrace(true);                    // 开启轨迹
producer.setTraceTopicName("RMQ_SYS_TRACE_TOPIC");  // 轨迹 Topic(默认值)

// 消费者开启轨迹
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group");
consumer.setEnableMsgTrace(true);
consumer.setTraceTopicName("RMQ_SYS_TRACE_TOPIC");

轨迹数据包含:

复制代码
消息轨迹数据(JSON):
{
  "traceType": "Pub",              // 类型:Pub 生产 / SubBefore 消费前 / SubAfter 消费后
  "timeStamp": 1723526400000,      // 时间戳
  "regionId": "DefaultRegion",
  "groupName": "producer_group",   // 组名
  "topic": "OrderTopic",           // Topic
  "msgId": "C0A8016400002A9F...",  // 消息 ID
  "tags": "TagA",
  "keys": "ORDER_10086",
  "storeHost": "192.168.1.100:10911",  // Broker 地址
  "clientHost": "192.168.1.50",        // 客户端地址
  "costTime": 5,                        // 耗时(ms)
  "status": 0                           // 状态
}

查询轨迹(控制台):

复制代码
RocketMQ Dashboard → 消息 → 按 Topic/Key/消息ID 查询 → 查看消息轨迹

轨迹链路示例:
[生产] 10:00:00.123 producer_group → OrderTopic(成功,耗时 5ms)
[消费前] 10:00:00.156 consumer_group(拉取到消息)
[消费后] 10:00:00.210 consumer_group(消费成功,耗时 54ms)

16.2 消息查询方式

查询方式 适用场景 说明
按 Topic 查询 模糊排查 时间范围 + Topic,可精确到 Queue
按消息 ID 查询 精确定位 已知 msgId,直接查询
按 Key 查询 业务定位 通过生产时设置的 Key 查询
bash 复制代码
# 命令行按 Key 查询
sh bin/mqadmin queryMsgByKey \
  -n localhost:9876 \
  -t OrderTopic \
  -k ORDER_10086

# 命令行按消息 ID 查询
sh bin/mqadmin queryMsgById \
  -n localhost:9876 \
  -i C0A8016400002A9F0000000000000000

16.3 消息轨迹排查实战

复制代码
问题:消费者没收到消息?

排查步骤:
① 控制台查询该消息的轨迹
② 看是否有 "Pub" 记录
    无 → 生产者发送失败(检查发送结果、重试日志)
    有 → 继续往下看
③ 看是否有 "SubBefore" 记录
    无 → 消费者组未订阅 / 订阅了错误 Tag / 消息还没到(延迟消息)
    有 → 继续往下看
④ 看 "SubAfter" 的 status
    status=成功 → 消息已消费,可能是消费逻辑问题
    status=失败 → 查看重试次数,是否进入死信队列
⑤ 检查死信队列 %DLQ%group

十七、Spring Boot 整合实战

17.1 引入依赖

xml 复制代码
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.3</version>
</dependency>

17.2 配置文件

yaml 复制代码
# application.yml
rocketmq:
  name-server: localhost:9876        # NameServer 地址
  producer:
    group: springboot_producer_group # 生产者组
    send-message-timeout: 3000       # 发送超时
    retry-times-when-send-failed: 2  # 失败重试次数
    max-message-size: 4194304        # 最大消息大小 4MB
  consumer:
    group: springboot_consumer_group # 默认消费组

17.3 生产者:RocketMQTemplate

java 复制代码
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /** 同步发送 */
    public void sendOrderCreated(Order order) {
        // 发送到 Topic:Tag
        rocketMQTemplate.convertAndSend("OrderTopic:TagA", order);
    }

    /** 异步发送 */
    public void sendAsync(Order order) {
        rocketMQTemplate.asyncSend("OrderTopic:TagB", order,
                new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        System.out.println("发送成功: " + sendResult.getMsgId());
                    }
                    @Override
                    public void onException(Throwable e) {
                        System.out.println("发送失败: " + e.getMessage());
                    }
                });
    }

    /** 发送带 Key 的消息(用于查询/去重) */
    public void sendWithKey(Order order) {
        Message<Order> msg = MessageBuilder
                .withPayload(order)
                .setHeader("KEYS", order.getOrderId())  // 消息 Key
                .build();
        rocketMQTemplate.send("OrderTopic:TagA", msg);
    }

    /** 发送顺序消息(按订单号路由) */
    public void sendOrdered(Order order) {
        rocketMQTemplate.syncSendOrderly(
                "OrderTopic:TagA",      // 目的地
                order,                   // 消息体
                order.getOrderId());     // 业务键(决定队列)
    }

    /** 发送延迟消息 */
    public void sendDelay(Order order) {
        // 第 2 个参数:延迟级别(3 = 10s)
        rocketMQTemplate.syncSend("OrderTopic:TagDelay", order, 3000, 3);
    }
}

17.4 消费者:@RocketMQMessageListener

java 复制代码
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

@Component
@RocketMQMessageListener(
        topic = "OrderTopic",                    // 订阅 Topic
        consumerGroup = "springboot_consumer_group",  // 消费者组
        selectorExpression = "TagA || TagB",     // Tag 过滤(* 表示全部)
        consumeMode = ConsumeMode.CONCURRENTLY,  // 并发消费(默认)
        // consumeMode = ConsumeMode.ORDERLY,    // 顺序消费
        messageModel = MessageModel.CLUSTERING   // 集群消费(默认)
        // messageModel = MessageModel.BROADCASTING,  // 广播消费
)
public class OrderConsumer implements RocketMQListener<Order> {

    @Override
    public void onMessage(Order order) {
        System.out.println("收到订单消息: " + order);

        // 注意:onMessage 方法抛出异常 = 消费失败 = 进入重试
        // 如果需要"消费成功但跳过",抛异常前自行处理
        try {
            // 业务处理:扣库存、发通知等
            handleOrder(order);
        } catch (Exception e) {
            // 记录日志后抛出,触发 RocketMQ 重试
            log.error("订单处理失败: {}", order, e);
            throw new RuntimeException("处理失败,触发重试", e);
        }
    }

    private void handleOrder(Order order) {
        // 幂等处理(重要!)
        if (idempotentService.alreadyProcessed(order.getOrderId())) {
            return;
        }
        // ... 实际业务逻辑
    }
}

17.5 完整项目结构

复制代码
spring-boot-rocketmq-demo/
├── pom.xml
├── src/main/java/com/example/rocketmq/
│   ├── RocketmqDemoApplication.java     # 启动类
│   ├── config/
│   │   └── RocketMQConfig.java          # 配置类(可空,默认即可)
│   ├── entity/
│   │   └── Order.java                   # 消息实体
│   ├── producer/
│   │   └── OrderProducer.java           # 生产者 Service
│   ├── consumer/
│   │   └── OrderConsumer.java           # 消费者
│   └── controller/
│       └── OrderController.java         # 测试接口
└── src/main/resources/
    └── application.yml                  # 配置

测试接口:

java 复制代码
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /** 测试:发送一条订单消息 */
    @PostMapping("/create")
    public String createOrder(@RequestBody Order order) {
        orderService.sendOrderCreated(order);
        return "订单消息已发送: " + order.getOrderId();
    }
}

17.6 事务消息(Spring Boot 版)

java 复制代码
// 方式一:使用 RocketMQTemplate 发送事务消息
rocketMQTemplate.sendMessageInTransaction(
        "TxTopic:TagA",      // 目的地
        order,               // 消息体
        null,                // 参数(传给事务监听器)
        new TransactionListener() {   // 事务监听器(内联实现)
            @Override
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                // 执行本地事务
                try {
                    orderMapper.insert(order);
                    return LocalTransactionState.COMMIT_MESSAGE;
                } catch (Exception e) {
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                }
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                // 回查:查询本地事务表
                return orderMapper.exists(msg.getKeys())
                        ? LocalTransactionState.COMMIT_MESSAGE
                        : LocalTransactionState.ROLLBACK_MESSAGE;
            }
        });

// 方式二:使用 @RocketMQTransactionListener 注解(推荐,职责分离)
@RocketMQTransactionListener
public class OrderTransactionListener implements RocketMQLocalTransactionListener {

    @Autowired
    private OrderMapper orderMapper;

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(
            org.springframework.messaging.Message msg, Object arg) {
        String orderId = (String) msg.getHeaders().get("KEYS");
        try {
            orderMapper.insert(...);
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(
            org.springframework.messaging.Message msg) {
        // 回查逻辑
        return RocketMQLocalTransactionState.COMMIT;
    }
}

十八、性能调优

18.1 生产者调优

参数 默认值 调优建议 说明
sendMsgTimeout 3000ms 按需调大 发送超时时间
retryTimesWhenSendFailed 2 2~3 同步发送失败重试次数
retryTimesWhenSendAsyncFailed 2 2~3 异步发送失败重试次数
批量发送 --- 开启 一次发送多条,吞吐提升明显
异步发送 --- 优先使用 比同步发送吞吐高
compressMsgBodyOverHowmuch 4096 保持默认 body 超 4KB 自动压缩

18.2 消费者调优

参数 默认值 调优建议 说明
consumeThreadMin/Max 20 按 CPU 核数调整 消费线程数(并发消费)
consumeMessageBatchMaxSize 1 1~32 批量消费
pullBatchSize 32 32~64 拉取批量
pullInterval 0 0 拉取间隔
maxReconsumeTimes 16 按需 重试次数
java 复制代码
// 消费者线程池调优示例
consumer.setConsumeThreadMin(10);
consumer.setConsumeThreadMax(40);      // 推荐:CPU 核数 × 2 ~ 4
consumer.setConsumeMessageBatchMaxSize(8);

18.3 Broker 调优

properties 复制代码
# broker.conf 关键调优参数

# ===== 存储 =====
# 消息保留时间(小时),默认 72
fileReservedTime=72
# 自动删除过期文件时间点
deleteWhen=04
# 异步刷盘(默认)
flushDiskType=ASYNC_FLUSH
# CommitLog 文件大小(默认 1G)
mapedFileSizeCommitLog=1073741824

# ===== 性能 =====
# 允许的最大消息大小(默认 4MB)
maxMessageSize=4194304
# 发送线程数(默认 1)
sendMessageThreadPoolNums=8
# 拉取线程数(默认 1)
pullMessageThreadPoolNums=16

# ===== 内存 =====
# 发送消息队列大小
sendMessageThreadPoolQueueCapacity=10000
# 拉取消息队列大小
pullThreadPoolQueueCapacity=100000

# ===== 高可用 =====
# 同步双写(提高可靠性,降吞吐)
brokerRole=SYNC_MASTER
# 异步刷盘 + 同步双写是"可靠性与吞吐"的最佳平衡
flushDiskType=ASYNC_FLUSH

18.4 JVM 调优

bash 复制代码
# bin/runbroker.sh 关键参数
JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g"
JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC"
JAVA_OPT="${JAVA_OPT} -XX:MaxGCPauseMillis=50"
# 堆外内存(page cache 依赖系统可用内存,预留足够空间给 OS)
# 建议:堆内存 : 堆外 = 1 : 1 左右

18.5 性能基准参考

场景 配置 结果
单 Broker 同步发送 4C8G + SSD 5~8 万 TPS
单 Broker 异步发送 4C8G + SSD 10~15 万 TPS
消费吞吐 单 Topic 4 队列 5~8 万 TPS(视业务)
消息延迟 同机房 P99 < 10ms

实际性能受消息大小、网络、消费逻辑、磁盘类型等多因素影响,以上为参考值。

18.6 性能调优 Checklist

复制代码
□ 生产端:优先异步发送 + 批量发送
□ 消费端:线程数 = CPU 核 × 2~4,开启批量消费
□ Broker:SSD 磁盘,独立挂载(不与其他 IO 竞争)
□ 队列数:Topic 队列数 ≥ 消费端并发数(默认 4 可调 8/16)
□ 预留 page cache 内存(JVM 堆 ≤ 物理内存 50%)
□ 关闭不必要的消息轨迹(有性能开销)
□ 使用压缩(大消息自动压缩)
□ 网络:客户端与 Broker 同机房/同 VPC

十九、监控运维

19.1 核心监控指标

指标 说明 告警阈值建议
消息积压量 最大位点 - 消费位点 > 10万 告警
生产 TPS 每秒生产消息数 与容量对比
消费 TPS 每秒消费消息数 下降异常告警
消费失败率 重试消息占比 > 1% 告警
死信队列消息数 处理失败的消息 > 0 立即告警
Broker 内存 JVM 堆使用率 > 80% 告警
磁盘使用率 存储磁盘 > 70% 告警,> 85% 紧急
磁盘写入速率 存储 IO 接近上限告警
客户端连接数 Broker 连接数 突变告警

19.2 常用运维命令(mqadmin)

bash 复制代码
# 查看集群状态
sh bin/mqadmin clusterList -n localhost:9876

# 查看 Topic 列表
sh bin/mqadmin topicList -n localhost:9876

# 查看 Topic 路由信息
sh bin/mqadmin topicRoute -n localhost:9876 -t OrderTopic

# 查看消费组进度(积压情况)
sh bin/mqadmin consumerProgress -n localhost:9876 -g consumer_group

# 查看消费组列表
sh bin/mqadmin consumerGroupList -n localhost:9876

# 创建 Topic
sh bin/mqadmin updateTopic -n localhost:9876 \
  -b 192.168.1.100:10911 -t NewTopic -p 8

# 删除 Topic
sh bin/mqadmin deleteTopic -n localhost:9876 -c DefaultCluster -t OldTopic

# 查看 Broker 状态
sh bin/mqadmin brokerStatus -n localhost:9876 -b 192.168.1.100:10911

# 查看 Broker 运行信息
sh bin/mqadmin brokerConsumeStat -n localhost:9876 -b 192.168.1.100:10911

# 查看消息积压
sh bin/mqadmin consumerProgress -n localhost:9876 -g group_name

19.3 积压告警脚本示例

bash 复制代码
#!/bin/bash
# check_rocketmq_backlog.sh - 检查消费积压

NAMESRV_ADDR="localhost:9876"
GROUP="order_consumer_group"
THRESHOLD=100000

# 获取消费进度(解析累计积压行)
backlog=$(sh /opt/rocketmq/bin/mqadmin consumerProgress \
  -n ${NAMESRV_ADDR} -g ${GROUP} 2>/dev/null \
  | grep -E "总积压|TOTAL" | awk '{print $NF}')

echo "消费组 ${GROUP} 积压: ${backlog}"

if [ "$backlog" -gt "$THRESHOLD" ]; then
  echo "⚠️ 积压超过阈值 ${THRESHOLD},触发告警"
  # 这里可以调用钉钉/企业微信 Webhook
  curl -s -X POST "https://oapi.dingtalk.com/robot/send?access_token=xxx" \
    -H "Content-Type: application/json" \
    -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"RocketMQ 积压告警: ${backlog}\"}}"
fi

19.4 常见故障排查

故障现象 排查思路
消息积压 ① 看消费 TPS ② 看消费失败日志 ③ 看消费者线程是否阻塞 ④ 检查依赖服务(DB/Redis)
消息丢失 ① 查轨迹(有无 Pub)② 查发送结果 ③ 检查刷盘/主从模式 ④ 检查消费端是否误删
消息重复 ① 检查消费幂等 ② 检查重平衡 ③ 检查广播模式
消费停止 ① 消费线程是否死锁/阻塞 ② 是否异常未处理 ③ 重平衡是否频繁
生产超时 ① 网络 ② Broker 负载 ③ 磁盘 IO ④ 是否触发限流
主从切换后不消费 ① 检查新 Leader 是否正常 ② 检查消费者路由刷新

二十、常见问题与最佳实践

20.1 常见问题

问题 1:消息重复消费

原因:RocketMQ 是"至少一次"语义,网络超时、重平衡、重试都可能导致重复。

解决:消费端幂等(见 11.6 节)。

问题 2:消息顺序被打破

原因

  • 发送端未使用 MessageQueueSelector 固定队列
  • 消费端使用了并发消费(CONCURRENTLY)
  • Topic 队列扩容导致队列重新分配

解决 :发送端 syncSendOrderly + 消费端 ConsumeMode.ORDERLY

问题 3:消费积压越来越严重

排查

复制代码
① 查看消费 TPS 与生产 TPS 的差距
② 查看消费端日志(是否有大量重试/异常)
③ 查看消费线程是否被阻塞(线程 dump)
④ 检查消费端依赖的下游(DB 连接池耗尽?Redis 慢?)
⑤ 临时扩容消费者(增加实例数 ≤ 队列数)

解决

  • 提升消费能力:增加实例、增加线程数、批量消费
  • 优化消费逻辑:减少 RPC 调用、异步化
  • 若历史积压过大,可新建临时 Topic 分流消费
问题 4:Broker 内存溢出

原因:JVM 堆设置过大导致 page cache 不足,或消息队列容量过大。

解决

bash 复制代码
# 调小 JVM 堆,给 OS page cache 留足空间
-Xms4g -Xmx4g -Xmn2g
# 或限制消息队列容量
sendMessageThreadPoolQueueCapacity=10000
问题 5:客户端连接不上

排查清单

复制代码
□ NameServer 地址是否正确
□ 9876/10911 端口是否放行(防火墙/安全组)
□ brokerIP1 配置是否客户端可达(常见问题:配了内网 IP,客户端在外网)
□ VIP 通道问题(10909 端口未放行)→ 设置 vipChannelEnabled=false
□ 客户端版本与 Broker 版本兼容

20.2 最佳实践清单

复制代码
✅ 生产端
□ 使用同步发送处理重要业务,异步发送提升吞吐
□ 设置消息 Key(业务主键),便于查询与幂等
□ 使用 Tag 做业务分类,但不要过多(建议 ≤ 20 个)
□ 消息体大小控制:< 1MB 最佳,最大 4MB
□ 设置合理的发送超时与重试次数

✅ 消费端
□ 消费逻辑必须幂等
□ 捕获业务异常,避免吞掉异常导致"假成功"
□ 消费逻辑尽量轻量,耗时操作异步化
□ 监控消费积压与死信队列
□ 使用集群消费实现负载均衡(默认)

✅ Topic 设计
□ Topic 按业务域划分(如 OrderTopic、PayTopic)
□ 队列数:根据消费并发度规划(默认 4,可调 8/16)
□ 避免 Topic 数量过多(一个 Broker 建议 < 100 个)
□ 命名规范:{业务域}{动作},如 OrderCreateTopic

✅ 集群与运维
□ NameServer ≥ 2 台
□ Broker 使用 DLedger 或 Controller 模式(生产必备)
□ 开启监控告警(积压、死信、磁盘、内存)
□ 定期巡检:磁盘空间、消息保留时间、消费进度
□ 大促前压测,评估容量

20.3 架构设计模式

复制代码
场景一:解耦(订单 → 库存/积分/通知)
┌──────┐  OrderTopic  ┌────────┐
│ 订单   │────────────▶│ 库存服务 │
│ 服务   │──┬─────────▶│ 积分服务 │
└──────┘  │  OrderTopic│ 通知服务 │
          └───────────▶└────────┘
  订单服务只发一次消息,三个下游各取所需

场景二:削峰(秒杀)
┌────────┐ 限流写入  ┌──────────┐  按消费能力  ┌────────┐
│ 秒杀入口 │─────────▶│ SecKillTopic │──────────▶│ 订单落库 │
└────────┘           └──────────┘              └────────┘
  100万请求写入队列    队列缓冲              下游从容处理

场景三:事务一致性(下单 + 扣库存)
下单服务(本地事务 + 事务消息)──> 扣库存服务
  要么都成功,要么都回滚(最终一致)

二十一、总结

21.1 核心知识体系

复制代码
RocketMQ 知识体系
│
├── 架构组件
│   ├── NameServer(注册中心)
│   ├── Broker(存储节点)
│   ├── Producer(生产者)
│   └── Consumer(消费者)
│
├── 核心概念
│   ├── Topic / Tag / Message
│   ├── Message Queue / Offset
│   ├── Consumer Group
│   └── CommitLog / ConsumeQueue
│
├── 消息类型
│   ├── 普通消息
│   ├── 顺序消息(MessageQueueSelector + Orderly)
│   ├── 延迟消息(18 个延迟级别)
│   ├── 事务消息(半消息 + 回查)
│   └── 批量消息
│
├── 可靠性保障
│   ├── 生产:同步发送 + 重试 + 事务消息
│   ├── 存储:同步刷盘 + 同步双写
│   ├── 消费:ACK + 重试 + 死信队列
│   └── 幂等:消费端必须做
│
├── 高可用
│   ├── 经典主从(手动切换)
│   ├── DLedger(Raft 自动选主)
│   └── Controller(5.x)
│
└── 运维能力
    ├── 消息轨迹 / 消息查询
    ├── ACL 权限控制
    ├── mqadmin 运维命令
    └── 监控告警(积压/死信/资源)

21.2 核心 API 速查表

场景 API
同步发送 producer.send(msg)
异步发送 producer.send(msg, callback)
单向发送 producer.sendOneway(msg)
顺序发送 producer.send(msg, selector, arg)
延迟消息 msg.setDelayTimeLevel(level)
事务消息 producer.sendMessageInTransaction(msg, arg)
批量发送 producer.send(List<Message>)
订阅 consumer.subscribe(topic, tag)
集群消费 consumer.setMessageModel(CLUSTERING)
广播消费 consumer.setMessageModel(BROADCASTING)
消费成功 return CONSUME_SUCCESS
消费失败 return RECONSUME_LATER
顺序消费成功 return SUCCESS
顺序消费失败 return SUSPEND_CURRENT_QUEUE_A_MOMENT

21.3 关键参数速查表

参数 位置 默认值 说明
namesrvAddr 客户端 必填 NameServer 地址
sendMsgTimeout Producer 3000ms 发送超时
retryTimesWhenSendFailed Producer 2 同步重试
consumeThreadMax Consumer 20 消费线程数
maxReconsumeTimes Consumer 16 重试次数
consumeMessageBatchMaxSize Consumer 1 批量消费
flushDiskType Broker ASYNC_FLUSH 刷盘策略
brokerRole Broker ASYNC_MASTER 主从模式
fileReservedTime Broker 72h 消息保留时间
autoCreateTopicEnable Broker true 自动建 Topic(生产关)

21.4 学习路线建议

复制代码
第一阶段:入门(1~2 天)
  概念理解 → 快速入门 Demo → 控制台使用

第二阶段:进阶(3~5 天)
  五种消息类型 → 可靠性机制 → 消费模式
  刷盘/复制 → 死信队列

第三阶段:实战(1 周)
  Spring Boot 整合 → 事务消息实战
  模拟订单/库存场景 → 性能测试

第四阶段:高级(持续)
  集群部署(DLedger)→ 源码阅读 → 监控体系
  故障演练 → 架构设计

如果本文对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续输出的动力!

相关推荐
笨鸟飞不快8 小时前
RocketMQ Lite Topic 流程梳理与设计解读
后端·rocketmq
Rain的Java大神之路2 天前
MQ重复消费问题怎么解决
java·经验分享·后端·面试·架构·rabbitmq·rocketmq
hopsky3 天前
《RocketMQ 技术内幕:RocketMQ 架构设计与实现原理》核心内容详解
rocketmq
小园子的小菜6 天前
RocketMQ 核心原理、架构优势与核心机制全解
架构·rocketmq
晚安code13 天前
RocketMQ 顺序消费实战:批量接口加 Redis Pipeline同步数据
redis·rocketmq
晚安code14 天前
RocketMQ 延迟消息实战:延迟双删策略解决 Redis 缓存一致性
redis·rocketmq
风样滴男人哟15 天前
spark streaming消费rocketmq的几种方式
大数据·spark·rocketmq
IT界的老黄牛15 天前
限流命中后该怎么办:直接丢、阻塞等待、延迟重投三种姿势的取舍
java·rocketmq·redisson·令牌桶·削峰·分布式限流
摇滚侠16 天前
《RocketMQ 官网》阅读笔记 RocketMQ 普通消息 延时消息 顺序消息 事务消息
笔记·rocketmq