消息中间件进阶:深入 RocketMQ 存储与原理
系列第三篇:存储与原理篇
你好,又见面了。
前两篇我们搞清楚了 RocketMQ 的整体架构和核心概念------知道了消息怎么流转、组件怎么协作、Topic 和 Queue 怎么回事。但从现在开始,我们要换个视角了。
如果说前两篇是站在"使用者的角度"看 RocketMQ,那么这一篇,我们要钻进 RocketMQ 的肚子里面,看看它到底是怎么存储消息的,为什么能那么快,又为什么能那么可靠。
这就好比你开一辆车,前两篇你学会了挂挡、踩油门、看后视镜;今天我们要打开引擎盖,看看发动机和变速箱是怎么工作的------只有理解了原理,你才能在遇到性能瓶颈或数据丢失风险时,知道问题出在哪,怎么去优化。
这篇文章会比较硬核,但我会尽量用通俗的语言和大量的图示,把 RocketMQ 最核心的存储机制掰开揉碎了讲给你听。准备好了吗?我们开始。
五、消息存储机制
CommitLog 的设计与原理
我们先从最核心的 CommitLog 说起。
CommitLog 是什么?
简单说,CommitLog 是 RocketMQ 存储消息的"主文件" 。每个 Broker 上只有一个 CommitLog 文件(严格说是有一组按大小滚动的文件),所有 Topic 的所有消息,都顺序写入到这个文件里。
你可以把 CommitLog 想象成一本巨大的流水账本------不管是谁的消息,来了就按顺序往本子上记,先来先记,后来后记,绝不跳着写。
为什么这么设计?
这里有个计算机存储的核心知识点:磁盘顺序写入的速度,比随机写入快几十甚至上百倍。
为什么?因为机械硬盘的读写依赖于磁头移动,随机写入意味着磁头要到处"跳",每次跳跃都需要寻道时间(平均约 5-10ms)。而顺序写入,磁头几乎不需要移动,数据像流水一样连续不断地写到磁盘上。
即使是 SSD,顺序写入也能更好地利用带宽,减少写放大效应。
RocketMQ 正是利用了这个特性,让所有消息都顺序追加到 CommitLog,从而获得了极高的写入吞吐量。
CommitLog 的文件结构
CommitLog 在磁盘上是一个文件集合,每个文件默认 1GB,文件名就是起始偏移量(用 20 位数字表示,不足补 0):
~/store/commitlog/
├── 00000000000000000000 (第1个文件,偏移量 0 开始)
├── 00000000001073741824 (第2个文件,偏移量 1GB 开始)
├── 00000000002147483648 (第3个文件,偏移量 2GB 开始)
└── ...
每个消息在 CommitLog 中的存储格式如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| 消息总长度 | 4 字节 | 整个消息条目的字节数 |
| 消息序号 | 8 字节 | 消息的唯一递增序号 |
| 存储时间戳 | 8 字节 | 消息存储时的时间戳 |
| 消息体长度 | 4 字节 | 消息体的字节数 |
| 消息体 | 变长 | 实际的消息内容 |
| 扩展属性 | 变长 | Topic、Tag、Key 等属性 |
| ... | ... | 其他元数据 |
这样的设计意味着写入 CommitLog 完全不区分 Topic,所有消息一视同仁,顺序追加------这也是 RocketMQ 写入性能极高的根本原因。
ConsumeQueue 的设计与原理
如果 CommitLog 是所有消息的大杂烩,那消费者怎么快速找到自己想要的消息呢?这就是 ConsumeQueue 的用武之地。
ConsumeQueue 是什么?
ConsumeQueue 是消息的"索引文件",每个 MessageQueue 对应一个 ConsumeQueue 文件。
如果说 CommitLog 是"流水账本",那 ConsumeQueue 就是"分类目录"------它不存储消息本身,只存储每条消息在 CommitLog 中的物理位置(偏移量),以及消息的大小和 Tag 的哈希值。
ConsumeQueue 的存储格式
每个 ConsumeQueue 条目固定 20 个字节,非常轻量:
| 字段 | 长度 | 说明 |
|---|---|---|
| CommitLog 偏移量 | 8 字节 | 消息在 CommitLog 中的物理位置 |
| 消息长度 | 4 字节 | 消息的字节数 |
| Tag 哈希码 | 8 字节 | Tag 的哈希值,用于消息过滤 |
这意味着,即使 CommitLog 有几十 GB,ConsumeQueue 也只有它的几十分之一大小,可以轻松加载到内存中,消费者查找消息时几乎不会产生磁盘 I/O。
💡 小贴士:这就是 RocketMQ 的"空间换时间"策略------用一个轻量的索引文件,让消息查找从 O(n) 变成了 O(1)。
CommitLog 与 ConsumeQueue 的协作关系
CommitLog 和 ConsumeQueue 是怎么配合工作的?下面这张图展示了它们之间的完整协作关系:
graph TB subgraph ProducerSide生产端 PProducer -->|发送消息| BBroker end subgraph Storage存储层 B -->|1. 顺序写入| CLCommitLog\
单一文件,所有消息共用 CL -->|2. 消息写入成功<br>返回偏移量| B CL -.->|3. 异步构建索引| ReputReputMessageService\
后台线程 Reput -->|4. 提取消息信息| CQ1ConsumeQueue\
TopicA-Queue0 Reput -->|4. 提取消息信息| CQ2ConsumeQueue\
TopicA-Queue1 Reput -->|4. 提取消息信息| CQ3ConsumeQueue\
TopicB-Queue0 end subgraph ConsumerSide消费端 CConsumer -->|5. 根据 Queue 和偏移量| CQ1 CQ1 -->|6. 返回 CommitLog 偏移量| C C -->|7. 根据物理偏移量<br>精准读取| CL CL -->|8. 返回消息体| C end style CL fill:#ffe0b2 style CQ1 fill:#c8e6c9 style CQ2 fill:#c8e6c9 style CQ3 fill:#c8e6c9 style Reput fill:#e1bee7
整个协作流程分为写入链路 和消费链路两条线:
写入链路(图中的 1→2→3→4):
- Producer 发送消息到 Broker
- Broker 将消息顺序写入 CommitLog(这一步就返回 ACK 给 Producer)
- 后台线程 ReputMessageService 异步地从 CommitLog 中解析消息
- 根据消息所属的 Topic 和 Queue,将索引信息写入对应的 ConsumeQueue
消费链路 (图中的 5→6→7→8):
-
Consumer 根据自己的消费进度(Queue 偏移量),查询 ConsumeQueue
-
ConsumeQueue 返回消息在 CommitLog 中的物理偏移量
-
Consumer 根据物理偏移量,直接从 CommitLog 读取消息内容
-
CommitLog 返回完整的消息体
注意,写入和构建索引是异步解耦的------消息一旦写入 CommitLog 就返回成功,索引的构建在后台慢慢追。这就是 RocketMQ 写入延迟极低的原因之一。
消息写入 CommitLog 的完整流程
一条消息从 Producer 发出到最终落盘,到底经历了哪些步骤?我们用一张详细的流程图来还原:
flowchart TD Start(Producer 发送消息) --> Step11. 消息到达 Broker Step1 --> Step22. 消息合法性校验\
Topic 是否存在 / 消息体大小是否超限 Step2 --> Step33. 消息内容准备\
生成消息 ID / 时间戳 / 计算 CRC Step3 --> Step4{4. 是否配置了<br>消息轨迹?} Step4 -->|是| Step4a记录消息轨迹数据 Step4a --> Step5 Step4 -->|否| Step55. 获取当前 CommitLog 文件的\
写入位置(全局锁) Step5 --> Step66. 将消息按固定格式序列化\
写入 CommitLog 的追加位置 Step6 --> Step77. 更新 CommitLog 的\
写入指针位置 Step7 --> Step8{8. 刷盘策略?} Step8 -->|同步刷盘| Step8a强制将数据从 PageCache\
刷入物理磁盘 Step8a --> Step9 Step8 -->|异步刷盘| Step8b数据仅在 PageCache 中\
等待后台线程异步刷盘 Step8b --> Step9 Step99. 返回写入结果给 Producer Step9 --> Step1010. 后台线程 ReputMessageService\
异步构建 ConsumeQueue 索引 Step10 --> End(消费者后续可消费到该消息) style Step6 fill:#ffe0b2 style Step8a fill:#ffcdd2 style Step8b fill:#c8e6c9 style Step10 fill:#e1bee7
步骤解析:
- 消息到达 Broker:Producer 通过网络将消息发送到 Broker 的指定端口
- 合法性校验:检查 Topic 是否存在、消息体是否超过 4MB(默认限制)等
- 消息内容准备:生成全局唯一的消息 ID,记录到达时间戳,计算 CRC 校验码
- 消息轨迹记录(可选):如果开启了消息轨迹功能,会记录消息的发送链路信息
- 获取写入位置 :通过全局锁 (
putMessageLock)获取当前 CommitLog 文件的写入偏移量------注意这里是加锁的,但因为是顺序写,锁的持有时间极短,不影响并发 - 序列化写入:将消息按照固定格式(魔数、消息体大小、消息体、扩展属性等)序列化为字节数组,追加到 CommitLog 文件末尾
- 更新指针:更新内存中的写入位置指针,为下一条消息做准备
- 刷盘:根据配置的刷盘策略,决定是立即刷盘还是只写到操作系统缓存(PageCache)
- 返回结果:将写入状态(成功/失败)和消息 ID 返回给 Producer
- 异步构建索引:后台线程异步构建 ConsumeQueue 索引,不影响主流程的响应速度
ConsumeQueue 的异步构建机制(ReputMessageService)
上面多次提到了 ReputMessageService,这是 RocketMQ 中一个非常关键的后台服务。我们来深入了解一下它到底是怎么工作的。
ReputMessageService 是什么?
它是 RocketMQ 中负责异步构建 ConsumeQueue 索引的后台线程服务。它像一个勤劳的"搬运工",不断从 CommitLog 中"搬运"消息的索引信息到对应的 ConsumeQueue 中。
为什么需要异步构建?
如果每写入一条消息,就同步去更新 ConsumeQueue,会有两个问题:
- 增加了写入链路的延迟(本来只需要写一次 CommitLog,现在要写两次)
- 无法保证 ConsumeQueue 的写入也是顺序的(不同 Topic/Queue 是分散的)
异步构建意味着:写入 CommitLog 就立即返回成功,索引在后台慢慢构建。
ReputMessageService 的工作流程:
sequenceDiagram participant Producer as Producer participant CL as CommitLog participant RMS as ReputMessageService participant CQ as ConsumeQueue participant Index as IndexFile Producer->>CL: 1. 发送消息,写入 CommitLog CL-->>Producer: 2. 返回写入成功(不等待索引) loop 每毫秒轮询 RMS->>CL: 3. 检查 CommitLog 是否有新数据 alt 有新数据 RMS->>RMS: 4. 解析新消息的物理位置和属性 RMS->>CQ: 5. 计算 ConsumeQueue 偏移量,写入索引条目 RMS->>Index: 6. 如果配置了 IndexFile,构建哈希索引 Note over RMS: 推动 CommitLog 的消费进度指针 (reputFromOffset) else 无新数据 RMS->>RMS: 短暂休眠(等待下一轮) end end
关键点:
- ReputMessageService 会记录一个
reputFromOffset指针,标记当前已构建到 CommitLog 的哪个位置 - 每次轮询时,从该位置读取新数据,构建索引,然后更新指针
- 如果某条消息的 Topic 或 Queue 不存在,ReputMessageService 会跳过并继续,同时记录错误日志
- 这种设计保证了即便索引构建失败,原始消息依然安全地存储在 CommitLog 中,数据不会丢失
消息的索引文件(IndexFile)的作用
除了 ConsumeQueue 这个"主索引",RocketMQ 还有一个 IndexFile(索引文件) ,用于支持根据消息 Key 查询消息。
IndexFile 是什么?
IndexFile 是一个基于哈希索引的查询文件,用于快速定位消息在 CommitLog 中的位置。
IndexFile 的结构:
graph TB subgraph IndexFileIndexFile 文件结构 Header文件头部\
(创建时间、消息总数、哈希槽数量等) Slot哈希槽数组\
(默认 500 万个槽位) Index索引条目数组\
(默认 2000 万条) end subgraph IndexItem索引条目结构(每条 20 字节) HashCodeKey 的哈希值\
4 字节 OffsetCommitLog 偏移量\
8 字节 Timestamp消息存储时间差\
4 字节 SlotNext同哈希槽的下一条索引\
4 字节 end Header --> Slot Slot --> Index style IndexFile fill:#e3f2fd
Key 查询流程:
- Producer 发送消息时指定 Key(例如订单 ID)
- Broker 在构建索引时,对 Key 进行哈希运算,存入 IndexFile
- 用户通过控制台或 API 查询时,输入 Key 值
- Broker 对 Key 进行同样的哈希运算,在 IndexFile 中找到对应的消息位置
- 再通过 CommitLog 偏移量读取完整消息
💡 小贴士 :ConsumeQueue 和 IndexFile 的区别在于------ConsumeQueue 用于按队列顺序 消费,IndexFile 用于按业务 Key 精确查询。一个是"扫货架",一个是"查字典"。
消息的物理文件布局与目录结构
了解了各个文件的作用,现在我们来看看它们在实际磁盘上是如何组织在一起的。RocketMQ 在 Broker 的存储目录下(默认是 ~/store),有这样一个完整的文件布局:
graph TB subgraph StoreDir\~/store/ (Broker 存储根目录) CommitLogDircommitlog/\
(所有消息的物理存储) ConsumeQueueDirconsumequeue/\
(消息的队列索引) IndexDirindex/\
(Key 查询索引) Checkpointcheckpoint\
(刷盘检查点文件) Abortabort\
(异常关闭标识) subgraph CommitLogFilescommitlog 目录 CL100000000000000000000\
1GB CL200000000001073741824\
1GB CL300000000002147483648\
1GB end subgraph ConsumeQueueFilesconsumequeue 目录 TopicATopicA\
TopicBTopicB\
TopicCTopicC\
end subgraph TopicAQueuesTopicA 目录下 Q0queue0\
Q1queue1\
Q2queue2\
Q3queue3\
end subgraph QueueFile每个 queue 目录下 CQFile00000000000000000000\
约 5.72MB end end CommitLogDir --> CommitLogFiles ConsumeQueueDir --> TopicA ConsumeQueueDir --> TopicB ConsumeQueueDir --> TopicC TopicA --> TopicAQueues TopicAQueues --> Q0 Q0 --> QueueFile style CommitLogDir fill:#ffe0b2 style ConsumeQueueDir fill:#c8e6c9 style IndexDir fill:#e3f2fd
各文件/目录的作用:
| 目录/文件 | 作用 |
|---|---|
commitlog/ |
存储所有消息的原始数据,每个文件 1GB,文件名=起始偏移量 |
consumequeue/{topic}/{queueId}/ |
存储 ConsumeQueue 索引,每个文件约 5.72MB(300000 条索引) |
index/ |
存储基于 Key 的哈希索引文件,用于消息查询 |
checkpoint |
记录最后一次刷盘的 CommitLog 和 ConsumeQueue 位置,用于宕机恢复 |
abort |
文件存在表示 Broker 异常关闭,启动时需要做恢复检查 |
消息文件的滚动与清理策略
RocketMQ 的文件不是无限增长的,它有完善的滚动 和清理机制。
文件滚动策略
- CommitLog :每个文件固定 1GB,写满后自动创建新文件
- ConsumeQueue :每个文件固定约 5.72MB(包含 300000 条索引),写满后自动创建新文件
文件名的设计非常巧妙------用文件的起始偏移量作为文件名。这样,通过任意一个偏移量,你可以立刻算出它属于哪个文件:
偏移量 15,000,000,000 → 文件名取整 → 00000000001500000000
文件清理策略
RocketMQ 的文件清理由 CleanCommitLogService 和 CleanConsumeQueueService 两个后台服务负责,清理的触发条件主要有三种:
flowchart TD Start(文件清理检查) --> Check1{磁盘空间<br>是否超过阈值?} Check1 -->|是| Force强制清理过期文件\
(最先删除最老的文件) Check1 -->|否| Check2{当前文件<br>是否已过期?} Check2 -->|是| Check3{过期时间<br>是否达到删除阈值?} Check3 -->|是| Delete删除过期文件 Check3 -->|否| Check4{删除周期<br>是否到达?} Check2 -->|否| Skip跳过,保留文件 Check4 -->|是| Delete Check4 -->|否| Skip Force --> End(结束) Delete --> End Skip --> End style Force fill:#ffcdd2 style Delete fill:#fff3e0 style Skip fill:#c8e6c9
具体策略:
- 文件过期删除 :Broker 配置了
fileReservedTime(默认 72 小时),文件被创建后超过这个时间,且文件不再被写入(即不是当前活跃文件),就会被删除 - 磁盘空间强制删除 :当磁盘使用率超过
diskMaxUsedSpaceRatio(默认 75%)时,会强制删除最老的文件,即使文件还未达到过期时间 - 手动触发:通过管理控制台或 API 手动触发清理
当前文件的保护:正在写入的 CommitLog 文件(当前活跃文件)不会被删除,即使它已经"过期"了。只有在文件滚动后,旧文件才会进入待删除队列。
消息的过期删除机制(文件时间戳与删除策略)
RocketMQ 的消息过期删除,本质上就是基于文件的删除,而不是基于单条消息的删除。这和很多其他消息中间件不同。
核心原理:
- 每个文件(CommitLog 文件或 ConsumeQueue 文件)都有一个最后修改时间戳
- 当后台清理线程扫描时,如果当前时间 - 文件最后修改时间 >
fileReservedTime(默认 72 小时),该文件就会被删除 - 因为 CommitLog 中的消息是按时间顺序写入的,所以删除文件 = 删除该时间点以前的所有消息
为什么要这样设计?
- 基于文件的删除比基于消息的删除高效得多(直接删除文件 vs. 遍历并标记删除)
- RocketMQ 假设消息在保存一段时间后,要么被消费了,要么不再需要了
- 这适用于大多数场景------消息是有"时效性"的,过期了就应该被清理
💡 小贴士 :如果你的业务有"长期保存消息"的需求(比如审计场景),可以设置更长的
fileReservedTime,也可以将消息转存到其他存储系统(如 OSS 或 HDFS)进行长期归档。
消息的零拷贝技术原理(MMAP + FileChannel)
接下来我们聊聊 RocketMQ 的性能黑科技------零拷贝。
什么是零拷贝?
传统的文件读取 → 网络发送,数据要经历多次拷贝:
- 从磁盘读到内核空间(DMA 拷贝)
- 从内核空间读到用户空间(CPU 拷贝)
- 从用户空间写到 Socket 缓冲区(CPU 拷贝)
- 从 Socket 缓冲区写到网卡(DMA 拷贝)
------ 4 次拷贝,2 次 CPU 参与,CPU 被占用做数据搬运,效率低。
零拷贝技术 是指通过操作系统的 sendfile 或 mmap 系统调用,减少数据在内核空间和用户空间之间的拷贝次数。
RocketMQ 在写入 和读取两个场景分别使用了不同的零拷贝技术:
写入场景:MMAP(内存映射文件)
RocketMQ 使用 FileChannel 的 map() 方法 ,将 CommitLog 文件映射到操作系统的虚拟内存中(PageCache 的映射)。
flowchart LR subgraph Traditional传统写入方式 App1应用程序 -->|1. 写入| UserBuf用户空间缓冲区 UserBuf -->|2. 系统调用| KernelBuf内核空间缓冲区\
PageCache KernelBuf -->|3. 异步| Disk1物理磁盘 Note1需要 1 次 CPU 拷贝 end subgraph MMAPRocketMQ 使用 MMAP App2应用程序 -->|直接写入映射内存| MMAPBuf内存映射区域\
(用户态可见的 PageCache) MMAPBuf -->|自动| Disk2物理磁盘 Note2零 CPU 拷贝\
由 MMU 硬件完成映射 end style Traditional fill:#ffcdd2 style MMAP fill:#c8e6c9
通过 MMAP,应用程序可以直接操作 PageCache 中的数据,省去了从用户态拷贝到内核态的过程。写入 CommitLog 时,数据直接写入 PageCache,由操作系统异步刷盘。
读取场景:sendfile(零拷贝网络传输)
当 Consumer 拉取消息时,RocketMQ 使用 FileChannel 的 transferTo() 方法 ,利用操作系统的 sendfile 系统调用,直接将数据从 PageCache 发送到网卡:
flowchart LR subgraph TraditionalRead传统读取 + 发送 Disk1物理磁盘 -->|DMA| Kernel1内核空间 Kernel1 -->|CPU 拷贝| User1用户空间 User1 -->|CPU 拷贝| Socket1Socket 缓冲区 Socket1 -->|DMA| Net1网卡 NoteCopy12 次 CPU 拷贝 end subgraph ZeroCopy零拷贝 sendfile Disk2物理磁盘 -->|DMA| Kernel2内核空间\
PageCache Kernel2 -->|DMA 直接传输<br>仅传递文件描述符和偏移量| Net2网卡 NoteCopy20 次 CPU 拷贝\
由 DMA 直接完成 end style TraditionalRead fill:#ffcdd2 style ZeroCopy fill:#c8e6c9
RocketMQ 为什么选择零拷贝?
- 减少 CPU 占用:CPU 不需要做数据搬运,可以专注处理业务逻辑
- 提高吞吐量:省去了多余的数据拷贝,数据传输更快
- 更低的延迟:减少了数据在内核/用户态之间的切换
💡 小贴士 :RocketMQ 的零拷贝是基于 PageCache 的。如果消息还在 PageCache 中,零拷贝直接从内存到网卡;如果消息已经被刷到磁盘,需要先读入 PageCache,再做零拷贝。这就是为什么刚生产的消息消费延迟极低(都在 PageCache 中)。
消息顺序写的性能优势(对比随机写)
我们经常听到"顺序写比随机写快",但到底快多少?为什么快?我们用一组对比来看清楚:
flowchart TB subgraph RandomWrite随机写入模式 HDD1硬盘磁头 -->|1. 寻道<br>移动磁头到位置 A| PosA位置 A HDD1 -->|2. 写入 4KB| PosA HDD1 -->|3. 寻道<br>移动到位置 B| PosB位置 B HDD1 -->|4. 写入 4KB| PosB HDD1 -->|5. 寻道<br>移动到位置 C| PosC位置 C HDD1 -->|6. 写入 4KB| PosC NoteRandom⚡ 每次写入 = 寻道时间 + 旋转延迟 + 传输时间\
💥 大量时间浪费在磁头移动上 end subgraph SequentialWrite顺序写入模式 HDD2硬盘磁头 -->|1. 初始寻道| StartPos起始位置 HDD2 -->|2. 顺序写入 4KB| Pos1位置 +4KB HDD2 -->|3. 顺序写入 4KB| Pos2位置 +8KB HDD2 -->|4. 顺序写入 4KB| Pos3位置 +12KB HDD2 -->|5. 顺序写入 4KB| Pos4位置 +16KB NoteSeq🚀 仅初始寻道一次\
后续几乎是纯数据传输\
速度可达 100-200MB/s end style RandomWrite fill:#ffcdd2 style SequentialWrite fill:#c8e6c9
性能差距到底有多大?
| 指标 | 随机写入 | 顺序写入 | 差距 |
|---|---|---|---|
| 机械硬盘吞吐量 | ~1-2 MB/s | ~100-200 MB/s | 100 倍 |
| SSD 吞吐量 | ~50 MB/s | ~500 MB/s | 10 倍 |
| IOPS(每秒操作数) | ~100-200 | ~10000+ | 50-100 倍 |
RocketMQ 如何利用顺序写?
- 所有消息都追加到同一个 CommitLog 文件,不区分 Topic,不随机跳转
- 单线程顺序写入 (由
putMessageLock保证),避免多线程竞争导致乱序 - 批量聚合:RocketMQ 支持批量发送消息,一次写入多条,进一步放大顺序写的优势
这就是 RocketMQ 能达到 十万级 TPS 的核心原因之一。
异步刷盘与同步刷盘策略
刷盘策略解决的是 "消息什么时候写到物理磁盘" 的问题。
异步刷盘
- 流程:消息写入 PageCache(操作系统缓存)后立即返回 ACK,由后台线程异步将数据刷入磁盘
- 优点:写入延迟极低(微秒级),吞吐量极高
- 缺点 :如果 Broker 宕机且 PageCache 中的数据尚未刷盘,数据可能丢失(但概率极低)
- 适用:对吞吐量要求高,容忍少量数据丢失的场景
同步刷盘
- 流程 :消息写入 CommitLog 后,调用
fsync()强制将数据刷入磁盘,等待刷盘完成才返回 ACK - 优点:数据可靠性极高,Broker 宕机也不会丢消息
- 缺点:写入延迟增加(毫秒级),吞吐量下降
- 适用 :金融、交易等一条都不能丢的场景
flowchart LR subgraph Async异步刷盘 A1消息写入 --> A2写入 PageCache\
立即返回 ACK A2 --> A3后台线程每隔 500ms\
批量刷盘 A3 --> A4物理磁盘 NoteA⚡ 延迟:微秒级\
⚠️ 宕机可能丢失少量数据 end subgraph Sync同步刷盘 S1消息写入 --> S2写入 PageCache S2 --> S3调用 fsync 强制刷盘 S3 --> S4刷盘完成,返回 ACK NoteS🛡️ 延迟:毫秒级\
✅ 保证数据零丢失 end style Async fill:#c8e6c9 style Sync fill:#ffcdd2
配置方式 :在 broker.conf 中配置 flushDiskType = ASYNC_FLUSH 或 SYNC_FLUSH。
同步复制与异步复制(主从数据同步机制)
注意 :刷盘是"主节点写磁盘",复制是"主节点往从节点同步数据"。这是两个不同的维度,可以组合使用。
异步复制
- Master 写入成功就返回 ACK,Slave 异步从 Master 拉取数据
- 优点:延迟低,吞吐量高
- 缺点:Master 宕机时,Slave 可能缺少部分数据
同步复制
- Master 写入后,等待 Slave 也写入成功才返回 ACK
- 优点:Master 宕机时,Slave 拥有完整数据
- 缺点:延迟增加,吞吐量降低
flowchart LR subgraph AsyncReplica异步复制 M1Master 写入 -->|数据同步| S1Slave M1 -->|直接返回 ACK<br>不等 Slave| Client1Producer NoteA⚡ 低延迟\
⚠️ 主从切换可能丢数据 end subgraph SyncReplica同步复制 M2Master 写入 -->|同步等待| S2Slave 写入 M2 -->|Slave 确认后<br>返回 ACK| Client2Producer NoteS🛡️ 高可靠性\
✅ 主从切换不丢数据 end style AsyncReplica fill:#c8e6c9 style SyncReplica fill:#ffcdd2
四种组合方式
| 刷盘方式 | 复制方式 | 可靠性 | 吞吐量 | 适用场景 |
|---|---|---|---|---|
| 异步刷盘 | 异步复制 | ⭐⭐ | ⭐⭐⭐⭐⭐ | 日志、监控,可丢少量数据 |
| 异步刷盘 | 同步复制 | ⭐⭐⭐ | ⭐⭐⭐⭐ | 普通业务,主从切换不丢数据 |
| 同步刷盘 | 异步复制 | ⭐⭐⭐⭐ | ⭐⭐⭐ | 核心业务,单机不丢数据 |
| 同步刷盘 | 同步复制 | ⭐⭐⭐⭐⭐ | ⭐⭐ | 金融级,任何情况都不丢数据 |
配置方式:在 broker.conf 中配置 brokerRole = ASYNC_MASTER / SYNC_MASTER / SLAVE。
RocketMQ 高性能的四大杀手锏总结
最后,我们来总结一下 RocketMQ 高性能背后的四大核心技术:
flowchart TB subgraph Killer1🔪 杀手锏一:顺序写入 K1_1所有消息追加到\
同一个 CommitLog K1_2利用磁盘顺序写\
比随机写快 100 倍 K1_3单线程写入保证\
无竞争开销 end subgraph Killer2🔪 杀手锏二:零拷贝技术 K2_1写入用 MMAP\
省去用户态→内核态拷贝 K2_2读取用 sendfile\
直接从 PageCache→网卡 K2_3CPU 不再做数据搬运\
专注业务逻辑 end subgraph Killer3🔪 杀手锏三:异步机制 K3_1异步刷盘\
写入 PageCache 即返回 K3_2异步构建索引\
ReputMessageService 后台处理 K3_3异步复制\
主从同步不阻塞写入 end subgraph Killer4🔪 杀手锏四:PageCache 利器 K4_1充分利用 OS 的\
PageCache 缓存 K4_2热数据全在内存\
读写近乎内存速度 K4_3OS 自动管理缓存\
淘汰冷数据 end style Killer1 fill:#ffeb3b,color:#000 style Killer2 fill:#4caf50,color:#fff style Killer3 fill:#2196f3,color:#fff style Killer4 fill:#ff9800,color:#fff
一句话总结:
RocketMQ 通过 顺序写入 获得极致的写入速度,通过 MMAP + sendfile 零拷贝 获得高效的数据传输,通过 异步机制 降低链路上每一环的延迟,通过 PageCache 让热数据读写如同内存操作。四大杀手锏环环相扣,共同铸就了 RocketMQ 在双十一万亿级流量下的卓越表现。
小结
这篇我们非常硬核地深入了 RocketMQ 的存储与原理层,通过 8 张核心图,搞清楚了:
- CommitLog 与 ConsumeQueue 是如何分工协作的(一个是流水账本,一个是分类目录)
- 消息写入的完整流程,以及 ReputMessageService 异步构建索引的机制
- IndexFile 如何支持基于 Key 的消息查询
- 物理文件布局、滚动和清理策略,理解消息是如何"过期"的
- 零拷贝技术(MMAP + sendfile) 的原理和优势
- 顺序写 vs 随机写 的巨大性能差距
- 刷盘策略(同步/异步) 和 复制策略(同步/异步) 的区别与组合
- 四大杀手锏 如何共同铸就 RocketMQ 的高性能
在理解了这些存储原理之后,下一篇我们将进入 消息发送与消费详解篇,看看 Producer 和 Consumer 的内部实现------消息是如何负载均衡的、消费进度是如何管理的、消息重试和死信又是怎么处理的。我们下期见!