深入 RocketMQ 内核:从整体架构到消息模型
三、RocketMQ 整体架构
RocketMQ 的四大核心组件:NameServer、Broker、Producer、Consumer
RocketMQ 的架构可以用四个词来概括,它们各司其职,配合默契:
NameServer:轻量级的“服务注册中心”和“路由表”,负责管理所有 Broker 的信息
Broker:真正干活的“消息存储服务器”,负责消息的写入、读取和持久化
Producer:消息的生产者,负责发送消息
Consumer:消息的消费者,负责拉取并处理消息
用人话打个比方:NameServer 是“通讯录”,Broker 是“仓库”,Producer 是“发货方”,Consumer 是“收货方”。
各组件职责与协作关系
它们之间是怎么协作的呢?单纯看文字很难形成立体感,下面这张全链路架构协作图能帮你一眼看清从消息发送到消费的完整数据流向:
存储层 - Broker 主从集群
注册中心层 - 无状态集群
客户端层
Broker 副本组 A
① 获取路由信息
② 发送消息
③ 获取路由信息
④ 拉取/消费消息
⑤ 定时心跳注册
⑤ 定时心跳注册
⑤ 定时心跳注册
⑥ 数据同步复制/异步复制
Broker 副本组 B
⑥ 数据同步
Master B
Slave B
Producer 生产者
Consumer 消费者
NameServer 1
NameServer 2
NameServer 3
Master A
(写入/读取)
Slave A
(数据备份/读取)
整个协作流程大致是这样的:
第一步:Broker 启动后,主动向所有 NameServer 节点注册自己的信息(IP、端口、存储的 Topic 信息等),并定时发送心跳。
第二步:Producer 发送消息前,先从 NameServer 拉取路由信息,知道消息应该发到哪个 Broker 去。
第三步:Producer 根据路由信息,把消息发到对应的 Broker 上。
第四步:Consumer 消费消息前,同样先从 NameServer 获取路由信息,知道去哪个 Broker 拉取消息。
第五步:Consumer 主动从 Broker 拉取消息进行消费(底层是 Pull 模式,Push 模式是在此基础上封装的)。
第六步:Broker Master 接收消息写入,Broker Slave 从 Master 同步数据,实现高可用。
整个流程环环相扣,但核心思想并不复杂:NameServer 负责“指路”,Broker 负责“收发货”,客户端跟着指引走。
NameServer 与 Broker 的关联方式
NameServer 是整个 RocketMQ 集群的“大脑”。虽然它不存储消息,但它知道所有 Broker 的信息。
Broker 启动的时候,会在配置文件中指定 NameServer 的地址列表。启动后,Broker 会向所有 NameServer 发送注册请求,把自己“登记”上去。之后每隔 30 秒,Broker 会发送一次心跳,告诉 NameServer:“我还活着”。
Producer 和 Consumer 在启动时,同样需要配置 NameServer 地址。它们会从 NameServer 拉取 Topic 对应的 Broker 路由信息,并缓存到本地。
NameServer 的搭建与 Broker 的搭建
在搭建 RocketMQ 集群时,通常的步骤是:
NameServer 搭建:
下载 RocketMQ 二进制包后,NameServer 的启动脚本在 bin/mqnamesrv
启动命令:nohup sh bin/mqnamesrv &
NameServer 本身非常轻量,不保存任何持久化数据,所有路由信息都在内存中
Broker 搭建:
Broker 的启动脚本是 bin/mqbroker
启动时需要指定配置文件:sh bin/mqbroker -c conf/broker.conf
配置文件中要指定 namesrvAddr(NameServer 地址列表)和 brokerClusterName(集群名称)
💡 小贴士:RocketMQ 5.x 推荐使用 Proxy + Broker 存算分离架构部署,其中 Proxy 是无状态的,可以独立水平扩展。不过对于理解核心原理,我们还是先从经典的 NameServer+Broker 架构入手。
NameServer 的多节点部署与无状态设计
NameServer 最巧妙的设计在于它的无状态性。
所谓“无状态”,指的是 NameServer 自身不持久化任何数据,所有的路由信息都是通过 Broker 的心跳上报动态构建的。这就意味着:
任意一个 NameServer 挂掉,其他节点照样能提供服务
NameServer 之间不需要互相通信,不需要选主,不需要数据同步
水平扩展极其简单——多启动几个实例就行
正因为 NameServer 节点之间完全对等、互不依赖,我们才能轻松部署 3 个甚至更多节点来保证高可用。客户端会连接所有 NameServer 节点,如果某个节点连不上,自动切换到下一个。
Broker 的 Master-Slave 主从架构
和 NameServer 不同,Broker 是有“身份”的。在 RocketMQ 4.x 及之前的经典部署模式中,Broker 分为两种角色:
Master(主节点):负责接收 Producer 的消息写入,也负责消息的读取。Master 可以配置多个,每个 Master 都有自己的 Slave。
Slave(从节点):不能接收写入,只能从 Master 同步数据,并承担一部分读请求(可以配置是否开启 Slave 读权限)。
Master 和 Slave 之间的数据同步方式有两种:
同步方式 特点 适用场景
同步复制 Master 收到消息后,等 Slave 也写入成功才返回 ACK 对数据可靠性要求极高,允许牺牲一点吞吐量
异步复制 Master 写入成功就返回 ACK,Slave 异步同步 追求高吞吐,能容忍极少量数据丢失
这里说的“同步复制”,指的是消息数据本身在主从间的同步策略。它是 RocketMQ 保证消息零丢失的重要手段——如果你的业务连磁盘损坏这种极端情况都不能丢消息,就必须配置同步复制。
Broker 集群的多种部署模式
RocketMQ 非常灵活,Broker 集群支持多种部署方式,你可以根据业务需求来选择。下面这张图清晰地展示了从简单到复杂的四种部署模式演进:
模式四:DLedger 多主多从
Leader
Follower
Follower
优点:自动故障切换
Raft 协议保障
模式三:多主多从
Master A
Slave A
Master B
Slave B
优点:高可用
缺点:需人工切主
模式二:多主无从
Master 1
无备份
Master 2
无备份
缺点:单主宕机数据不可达
模式一:单主单从
Master
Slave
缺点:存在单点故障
- 单主模式(单 Master)
只有一个 Broker,没有 Slave。部署最简单,但存在单点故障。
适用:开发测试环境。
2. 多主模式(多 Master,无 Slave)
多个 Master 节点,每个 Master 存储不同的 Topic 分区数据。吞吐量高,但某个 Master 宕机后,该节点上的消息在恢复前无法消费。
适用:对可用性要求不高、允许少量数据暂时不可达的业务。
3. 多主多从模式(多 Master,每个 Master 带 Slave)
生产环境的标准选择。每个 Master 至少有一个 Slave 作为备份,高可用。
缺点:Master 宕机后,虽然 Slave 可读,但写服务会停,且需要人工介入切换。
4. 多主多从 + Dledger 模式(自动故障切换)
适用:核心交易链路、金融级业务。
Dledger 高可用架构与自动故障切换
在传统的多主多从模式下,如果 Master 挂了,虽然 Slave 可以继续提供读服务,但写服务就停了,而且需要人工介入把 Slave 切换为 Master。
Dledger 的出现解决了这个问题。Dledger 是 RocketMQ 基于 Raft 共识算法实现的高可用解决方案。当 Leader(即 Master)挂掉时,集群如何秒级完成自动切换?下图展示了它的内部选举流程:
客户端/监控
Follower B
Leader (Master)
Follower A
客户端/监控
Follower B
Leader (Master)
Follower A
Leader 正常运行
处理读写请求
💥 Leader 宕机!
进入选举超时计时 (Election Timeout)
成为新 Leader
同步数据
同步数据
连接中断
发起投票请求 (RequestVote)
投票响应 (Vote Granted)
获得多数派投票 (1/2)
恢复服务
(秒级切换,无需人工干预)
在 Dledger 模式下,一组 Broker 节点(通常为 3 个)组成一个副本组。当 Leader 挂了之后,剩余的 Follower 会自动重新选举出新的 Leader,整个过程无需人工干预,秒级完成切换。
NameServer 与 Broker 的启动顺序与关联流程
部署时,启动顺序很重要:
正确的启动顺序:
先启动 NameServer(所有节点)
再启动 Broker(所有节点)
如果 Broker 启动时 NameServer 还没启动,Broker 会不断重试连接,直到连上为止。
NameServer 的心跳检测机制与 Broker 故障剔除
NameServer 是怎么知道一个 Broker 是“活着”还是“死了”的?答案就是心跳检测机制。这套机制非常精巧,完全是 Broker 主动上报、NameServer 被动超时剔除的模式,极大地降低了 NameServer 的压力。我们通过下面这张时序图来看清全流程:
Producer/Consumer
NameServer 集群
Broker 节点
Producer/Consumer
NameServer 集群
Broker 节点
记录心跳时间
loop
[每 30 秒 (心跳上报)]
alt
[当前时间 - 最后存活时间
120 秒]
loop
[每 10 秒 (故障扫描)]
alt
[路由表发生变更 (Broker 被剔除)]
loop
[每 30 秒 (客户端拉取)]
发送心跳包 (包含存活状态、路由信息)
更新 Broker 的最后存活时间戳
扫描 Broker 存活列表
判定 Broker 失联,从路由表剔除
更新内存中的路由表
拉取最新的 Topic 路由信息
返回最新的 Broker 列表 (已剔除故障节点)
更新本地缓存,不再往故障 Broker 发消息
Broker 主动上报:Broker 每隔 30 秒向所有 NameServer 发送一次心跳,心跳里包含了 Broker 的当前状态、存储容量、Topic 信息等
NameServer 主动超时剔除:NameServer 会定期扫描自己维护的 Broker 列表。如果一个 Broker 连续 120 秒(默认) 没有发送心跳,NameServer 就会认为它“失联”了,将其从路由表中移除
客户端感知变化:Producer 和 Consumer 每隔 30 秒会从 NameServer 拉取最新的路由信息。一旦某个 Broker 被剔除,客户端在下次拉取时就能感知到,从而不再往“死”掉的 Broker 发送消息
四、Topic 与消息模型
搞清楚了架构层面的组件协作,现在我们向下深挖一层,来到 RocketMQ 中最核心的数据模型——Topic。
Topic 与 MessageQueue 的关系
在 RocketMQ 中,Topic 是一个逻辑概念,MessageQueue 是物理概念。
打个比方:Topic 就像是一个“主题文件夹”,而 MessageQueue 是这个文件夹下面的“物理文件”。
Topic:生产者发送消息时指定 Topic,消费者订阅时指定 Topic。它是消息的分类标签。
MessageQueue:Topic 下面实际存储消息的最小物理单元。一条消息最终会被写到某个具体的 MessageQueue 里。
一个 Topic 下面有多个 MessageQueue(默认是 4 个)。生产者发消息时,会根据负载均衡策略(比如轮询、哈希取模)选择其中一个 Queue 来发送。
Topic 与 Broker 的关系:多对多映射
Topic 和 Broker 之间的关系是多对多的:
一个 Topic 可以分布在多个 Broker 上(每个 Broker 上存放该 Topic 的部分 Queue)
一个 Broker 上也可以存放多个 Topic 的 Queue
这种多对多的映射关系,让 RocketMQ 具备了水平扩展的能力。举个例子:如果你的 Topic 流量太大,一个 Broker 扛不住,你可以在更多 Broker 上创建这个 Topic 的 Queue,把流量分散开。
物理节点:Broker Master 2
物理节点:Broker Master 1
逻辑概念:Topic: order_topic
映射
映射
映射
映射
Queue 0
Queue 1
Queue 2
Queue 3
Queue 0
Queue 1
Queue 0
Queue 1
如图,order_topic 的 4 个 Queue 分布在 2 个 Broker 上,每个 Broker 各存 2 个 Queue。这种设计不仅实现了存储的负载均衡,也为后续的高可用打下了基础。
MessageQueue 的物理存储结构
这是 RocketMQ 最精妙的设计之一,也是它为什么能具备极高写入吞吐量的核心机密。RocketMQ 采用了 CommitLog + ConsumeQueue 的两层存储结构。
光说概念可能有点抽象,我们来看下面这张物理存储结构图,它能帮你直观地理解“顺序写入”和“索引读取”是如何共存的:
消费流程(快速定位)
异步索引构建
写入流程(极高吞吐量)
-
顺序追加写入
-
异步构建索引
-
异步构建索引
-
异步构建索引
-
根据逻辑偏移量查询
-
返回物理偏移量 offset=1024
-
根据物理偏移量精准读取
Producer
CommitLog
(单一物理文件,所有 Topic 共用)
所有消息无论属于哪个 Topic
都按到达顺序依次追加到 CommitLog
ConsumeQueue: order_topic-0
(存储偏移量 + 大小 + Tag 哈希)
ConsumeQueue: order_topic-1
ConsumeQueue: user_topic-0
Consumer
CommitLog(仓库):所有消息顺序写入同一个大文件(每个 Broker 只有一个 CommitLog)。无论消息属于哪个 Topic,都顺序追加到 CommitLog 里——这种顺序写入的方式让 RocketMQ 拥有了极高的写入吞吐量(因为磁盘顺序写远快于随机写)。
ConsumeQueue(货架标签):每个 MessageQueue 对应一个 ConsumeQueue 文件,它相当于一个“索引”,里面记录了每条消息在 CommitLog 中的物理偏移量、大小、Tag 哈希值等信息。
Consumer 消费时,先查 ConsumeQueue 找到消息在 CommitLog 里的位置,再去 CommitLog 读取消息内容。
为什么一个 Topic 需要多个队列?
这可能是很多初学者最困惑的问题:为什么一个 Topic 下面要有多个 Queue?
核心原因有三个:
- 提高并发吞吐量
如果 Topic 只有一个 Queue,那么无论你有多少个消费者实例,同时只能有一个消费者从该 Queue 拉取消息(为了避免重复消费)。这就成了性能瓶颈。
多个 Queue 意味着多个消费者可以并行消费,吞吐量成倍提升。
- 实现顺序消息
RocketMQ 的顺序消息,保证的是同一个 Queue 内的消息严格有序。所以如果你要保证某一批消息的顺序(比如同一个订单的创建→支付→发货),只需要把它们的 Key(比如订单 ID)用哈希取模分配到同一个 Queue 即可。
- 负载均衡
多个 Queue 分布在不同的 Broker 上,可以实现存储和流量的负载均衡。
路由发现机制:Producer/Consumer 如何从 NameServer 获取路由信息?
路由发现是客户端和 NameServer 之间的核心交互流程。客户端并不是每次都去查 NameServer,而是有本地缓存和定时更新的机制,如下图所示:
是
有变化
无变化
客户端启动
从配置读取 NameServer 地址列表
尝试建立连接
发送拉取路由请求
(携带 Topic 名称)
NameServer 返回 Broker 地址列表
及 Queue 分布信息
缓存路由信息到本地内存
定时器触发
(默认 30 秒)
再次拉取最新路由
与本地缓存对比
是否有变化?
更新本地缓存
保持不变
发消息/消费时
直接从本地缓存获取 Broker
客户端启动时:Producer 或 Consumer 从配置的 NameServer 地址列表中,选择一个可用的 NameServer 节点建立连接
拉取路由:客户端发送请求,携带 Topic 名称,NameServer 返回该 Topic 对应的所有 Broker 信息(包括 Master 和 Slave 的 IP、端口、Queue 分布等)
缓存更新:客户端将路由信息缓存在本地,同时启动一个定时任务(默认 30 秒),定期从 NameServer 拉取最新路由
异常处理:如果客户端连接某个 Broker 失败,会主动向 NameServer 重新拉取路由,看是不是路由信息发生了变化
💡 小贴士:路由信息在客户端是有缓存的,不会每次发消息都去查 NameServer,所以 NameServer 不会成为性能瓶颈。
队列数量与消费并行度的关系
这是一个非常重要的概念:消费者的最大并行度 = 该 Topic 的 MessageQueue 数量。
为什么?因为 RocketMQ 的集群消费模式下,一个 Queue 在同一时刻只能被同一个消费者组里的一个消费者实例消费。
下面这张图完美地展示了 Queue 数量与消费者实例数量之间的负载均衡关系,以及为什么多余的消费者会处于空闲状态:
消费者过少:队列数 > 消费者数
Queue 1
Consumer 实例 1
Queue 2
Queue 3
Consumer 实例 2
Queue 4
并行度 = 2
未能充分利用队列并发
消费者过多:队列数 < 消费者数
Queue 1
Consumer 实例 1
Queue 2
Consumer 实例 2
Queue 3
Consumer 实例 3
Queue 4
Consumer 实例 4
Consumer 实例 5
处于空闲等待状态
⚠️ 资源浪费
最大并行度仍为 4
理想情况:队列数 = 消费者数
Queue 1
Consumer 实例 1
Queue 2
Consumer 实例 2
Queue 3
Consumer 实例 3
Queue 4
Consumer 实例 4
并行度 = 4
硬件资源充分利用
场景 A(理想):4 个 Queue 配 4 个消费者,完美并行,性能拉满。
场景 B(过多):4 个 Queue 配 5 个消费者,第 5 个完全空闲,白白浪费资源。
场景 C(过少):4 个 Queue 配 2 个消费者,每个消费者扛 2 个 Queue,并发度只有 2。
结论:通常建议 消费者实例数量 ≤ Queue 数量,且 Queue 数量最好是消费者数量的整数倍,这样分配最均匀。
如何合理规划 Topic 和队列数量?
这是一个实战中经常遇到的问题,没有一个“万能数字”,但可以参考以下经验:
队列数量规划的黄金法则:
场景 建议 Queue 数量 说明
普通业务(TPS < 1000) 4~8 个 够用且轻量
中等流量(TPS 1000~10000) 8~16 个 根据实际压力测试调整
高并发场景(TPS > 10000) 16~32 个 配合更多 Broker 分散压力
严格顺序消息场景 根据业务 Key 的分布来定 比如按订单 ID 哈希,Queue 数最好远大于消费者数,保证分布均匀
另外几个原则:
Queue 数量一旦确定,后期调整比较麻烦(虽然 RocketMQ 5.x 支持动态扩缩,但生产环境建议谨慎),所以初期可以稍微设大一点
Queue 数量要和 Broker 数量配合,尽量均匀分布
不要盲目设大——每个 Queue 都要消耗内存和文件句柄,过多的 Queue 会增加 Broker 的负担
Tag 的作用与使用场景
在 RocketMQ 中,Tag(标签) 是 Topic 之下的第二级消息分类。
想象一下,你有一个 order_topic,里面既有下单消息(order_create),也有支付消息(order_pay),还有退款消息(order_refund)。如果用三个独立的 Topic 来区分,管理起来太繁琐了——这时候 Tag 就派上了用场。
Consumer 在订阅 Topic 时,可以用 Tag 来过滤消息,只消费自己感兴趣的那一类。
Tag 的使用方式:
生产者发送时指定 Tag:SendResult result = producer.send(msg, “order_create”);
消费者订阅时指定 Tag:consumer.subscribe(“order_topic”, “order_create || order_pay”);
适用场景:
同一业务类型下的不同子类型区分
消费者只关注某个 Topic 下的部分消息,节省网络传输和处理开销
Tag 与 SQL92 过滤方式的对比
RocketMQ 提供了两种消息过滤方式:Tag 过滤和 SQL92 过滤。它们在 Broker 内部的处理逻辑完全不同,下图对比了这两种过滤方式的执行路径:
方式二:SQL92 过滤(灵活)
条件为 TRUE
条件为 FALSE
消息携带 属性: age>18, name=Tom
Broker 接收
Broker 解析 SQL 表达式
遍历消息属性进行条件判断
发送给 Consumer
丢弃
性能:消耗 CPU
适用于复杂业务规则筛选
方式一:Tag 过滤(高效)
匹配成功
匹配失败
消息携带 Tag_A
Broker 接收
根据 ConsumeQueue 中的 Tag 哈希码
进行快速匹配
发送给 Consumer
丢弃,不网络传输
性能:极高
适用于简单分类
对比维度 Tag 过滤 SQL92 过滤
过滤能力 仅支持精确匹配和 || 组合 支持复杂的条件表达式(> < = AND OR 等)
性能 极高(Broker 侧基于哈希码快速过滤) 较低(需要解析 SQL 表达式,遍历消息属性)
灵活性 低(只能打一个 Tag,且只能是精确匹配) 高(可以基于多个用户自定义属性进行复杂过滤)
适用场景 简单分类,对性能要求极高 复杂筛选逻辑,对实时性要求不高
💡 小贴士:SQL92 过滤依赖于 Broker 的 enablePropertyFilter 配置,默认是开启的。但它会消耗更多 Broker 的 CPU 资源,所以能走 Tag 过滤就尽量用 Tag。
消息的 Key 与业务查询
最后,我们来聊聊消息的 Key。
Key 是消息的一个自定义属性,通常用来标识业务的唯一键。比如订单消息可以用 orderId 作为 Key,支付消息可以用 paymentId 作为 Key。
Key 有什么用?
-
消息查询:RocketMQ 提供了根据 Key 查询消息的功能。当你在控制台输入订单号查询消息时,背后利用的就是 Key 索引。这个索引的核心是Hash 索引——消息发送时,Broker 会根据 Key 的哈希值构建索引条目,存储在索引文件中,查询时通过哈希快速定位到具体的消息。
-
幂等处理:Consumer 可以根据 Key 来做幂等判断,防止消息重复消费导致业务数据出错。
Key 的设计建议:
尽量用业务上唯一且不变的字段(如订单 ID、用户 ID)
如果一条消息包含多个业务维度,选最核心的那个作为 Key
不建议把时间戳、随机数这类变化频繁的值作为 Key(无法精准定位)
💡 小贴士:RocketMQ 的消息体最大支持 4MB(5.x 版本可通过配置调整),但如果你的消息体很大(比如超过 1MB),建议考虑压缩或把大内容存到 OSS,消息里只存引用路径。
更多推荐




所有评论(0)