RocketMQ 使用详解:从入门到实战
消息中间件系列文章 · 第二弹 ------ 继 RabbitMQ 之后,带你全面掌握 RocketMQ 的架构、使用与实战
目录
- [一、RocketMQ 概述](#一、RocketMQ 概述)
- 二、核心概念与架构
- 三、安装部署
- 四、快速入门:第一个生产者与消费者
- 五、普通消息详解
- 六、顺序消息
- 七、延迟消息
- 八、事务消息
- 九、批量消息
- 十、消费模式:集群消费与广播消费
- [十一、消息可靠性:ACK 与重试机制](#十一、消息可靠性:ACK 与重试机制)
- 十二、死信队列
- 十三、刷盘策略与主从复制
- [十四、集群部署:DLedger 模式](#十四、集群部署:DLedger 模式)
- [十五、ACL 权限控制](#十五、ACL 权限控制)
- 十六、消息轨迹与消息查询
- [十七、Spring Boot 整合实战](#十七、Spring Boot 整合实战)
- 十八、性能调优
- 十九、监控运维
- 二十、常见问题与最佳实践
- 二十一、总结
RabbitMQ使用详解.md
一、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 通道 (
brokerIP1与brokerIP2不一致时触发),否则客户端可能连不上。可在 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 的价值:
- 快速查询:在控制台/管理端按 Key 检索消息
- 幂等去重:消费端可用 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 主动回查) │ 直到拿到明确结果
▼
核心流程:
- 发送半消息:Producer 发送消息,Broker 存储但不投递(对消费者不可见)
- 执行本地事务:Producer 收到半消息成功后,执行本地事务(如写订单表)
- 提交/回滚 :本地事务执行完成后,Producer 向 Broker 提交(Commit)或回滚(Rollback)
- Commit → 消息对消费者可见,正常投递
- Rollback → 消息被删除,不投递
- 消息回查 :如果 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)→ 源码阅读 → 监控体系
故障演练 → 架构设计
如果本文对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续输出的动力!