06-04-A-RocketMQ存储深水区与源码解析详解
️ 关键词:MappedFile · transientStorePool · ReputMessageService · 长轮询 PullRequestHoldService · 刷盘服务 · 文件恢复 · ConsumeQueue 重建 · Pop 消费 · 5.x 分层存储
📌 导读 :01 篇讲了 RocketMQ 存储的"骨架"(CommitLog+ConsumeQueue+IndexFile),本篇下潜到源码级深水区------MappedFile 与堆外内存池(transientStorePool)的读写分离、消息写入的锁演进(ReentrantLock vs 自旋)、ReputMessageService 如何异步构建索引、长轮询 PullRequestHoldService 的实现、同步/异步刷盘服务的线程模型、Broker 崩溃后的文件恢复与 ConsumeQueue 重建、5.x 的 Pop 消费与分层存储(Tiered Storage)。看完本篇你能做到:被问"RocketMQ 写入为什么要用堆外内存池"能讲出读写分离与 PageCache 毛刺,被问"Broker 宕机重启怎么恢复"能画出恢复流程,被问"长轮询怎么实现"能讲出 HoldService 的挂起-唤醒机制。
📑 目录
- 06-04-A-RocketMQ存储深水区与源码解析详解
-
- [📖 术语速查表(每个词都用人话解释)](#📖 术语速查表(每个词都用人话解释))
- [一、MappedFile 与堆外内存池:写入路径的源码视角](#一、MappedFile 与堆外内存池:写入路径的源码视角)
-
- [1.1 MappedFile 的三块内存](#1.1 MappedFile 的三块内存)
- [1.2 transientStorePool:为什么要多一块堆外内存](#1.2 transientStorePool:为什么要多一块堆外内存)
- [1.3 文件预热(warmMappedFile)](#1.3 文件预热(warmMappedFile))
- [1.4 写入锁的演进:putMessageLock](#1.4 写入锁的演进:putMessageLock)
- 二、ReputMessageService:索引是怎么异步构建的
-
- [2.1 分发流程](#2.1 分发流程)
- [2.2 三个源码级结论](#2.2 三个源码级结论)
- [三、长轮询:PullRequestHoldService 的挂起与唤醒](#三、长轮询:PullRequestHoldService 的挂起与唤醒)
-
- [3.1 为什么长轮询是"伪 Push"](#3.1 为什么长轮询是"伪 Push")
- [3.2 关键参数对照](#3.2 关键参数对照)
- 四、刷盘服务与崩溃恢复
-
- [4.1 两种刷盘服务的线程模型](#4.1 两种刷盘服务的线程模型)
- [4.2 崩溃恢复流程](#4.2 崩溃恢复流程)
- [4.3 ConsumeQueue 损坏的救法](#4.3 ConsumeQueue 损坏的救法)
- [五、5.x 新存储特性:Pop 消费与分层存储](#五、5.x 新存储特性:Pop 消费与分层存储)
-
- [5.1 Pop 消费:无状态消费模式](#5.1 Pop 消费:无状态消费模式)
- [5.2 分层存储(Tiered Storage)](#5.2 分层存储(Tiered Storage))
- 六、跑一遍:观察堆外池与恢复行为
-
- [6.1 查看存储层运行时状态](#6.1 查看存储层运行时状态)
- [6.2 模拟异常宕机观察恢复](#6.2 模拟异常宕机观察恢复)
- 七、总结
-
- [7.1 一张图回顾全文](#7.1 一张图回顾全文)
- [7.2 核心要点浓缩(十二条)](#7.2 核心要点浓缩(十二条))
📖 术语速查表(每个词都用人话解释)
| 术语 | 一句话白话解释 |
|---|---|
| MappedFile | CommitLog/HFile 的文件抽象------封装 mmap 映射、写入位置、预热逻辑,是存储层的最小单元 |
| MappedFileQueue | MappedFile 的链表管理------滚动创建新文件、过期删除旧文件 |
| transientStorePool | 堆外内存池 (TransientStorePool)------写入先进堆外 ByteBuffer,异步 commit 到 FileChannel------读写分离,写不抢 PageCache |
| commit / flush | 堆外池模式的两段落盘:commit = 堆外→FileChannel(进 PageCache) ,flush = FileChannel→磁盘 |
| putMessageLock | 写入锁------ReentrantLock(默认)或自旋锁(ReentrantLockOptional),高并发下的选择 |
| ReputMessageService | 索引构建线程------从 CommitLog 读新消息,异步分发构建 ConsumeQueue 和 IndexFile |
| CommitLogDispatcher | 分发器接口------DispatcherBuildConsumeQueue / DispatcherBuildIndex 两个实现 |
| PullRequestHoldService | 长轮询挂起服务------没消息时把拉取请求挂起(默认 5s 检查一次),新消息到达立即唤醒 |
| GroupCommitService | 同步刷盘服务------生产者线程提交请求后等待刷盘完成(GroupCommitRequest 的 future) |
| FlushRealTimeService | 异步刷盘服务------后台定时(500ms)把 PageCache 刷盘 |
| 文件恢复 | Broker 重启时从倒数第 3 个文件开始校验(magic+CRC),找到最后一个完整文件截断恢复 |
| abort 文件 | Broker 启动创建、正常关闭删除的哨兵文件------存在 = 上次异常宕机,走完整恢复流程 |
| Pop 消费 | 5.x 新消费模式------无状态消费(Broker 管位点+不可见时间),替代"Queue 独占"的 Push 模式 |
| 分层存储(Tiered Storage) | 5.x 特性------冷数据自动下沉到对象存储(OSS/S3),本地只留热数据 |
一、MappedFile 与堆外内存池:写入路径的源码视角
1.1 MappedFile 的三块内存
#mermaid-svg-3pQqYYS7rr7zgW1S{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3pQqYYS7rr7zgW1S .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3pQqYYS7rr7zgW1S .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3pQqYYS7rr7zgW1S .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-3pQqYYS7rr7zgW1S .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-3pQqYYS7rr7zgW1S .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3pQqYYS7rr7zgW1S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3pQqYYS7rr7zgW1S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3pQqYYS7rr7zgW1S .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3pQqYYS7rr7zgW1S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3pQqYYS7rr7zgW1S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3pQqYYS7rr7zgW1S .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-3pQqYYS7rr7zgW1S .marker.cross{stroke:#0b0b0b;}#mermaid-svg-3pQqYYS7rr7zgW1S svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-3pQqYYS7rr7zgW1S p{margin:0;}#mermaid-svg-3pQqYYS7rr7zgW1S .label{font-family:Microsoft YaHei;color:#333;}#mermaid-svg-3pQqYYS7rr7zgW1S .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-3pQqYYS7rr7zgW1S .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-3pQqYYS7rr7zgW1S .cluster-label span p{background-color:transparent;}#mermaid-svg-3pQqYYS7rr7zgW1S .label text,#mermaid-svg-3pQqYYS7rr7zgW1S span{fill:#333;color:#333;}#mermaid-svg-3pQqYYS7rr7zgW1S .node rect,#mermaid-svg-3pQqYYS7rr7zgW1S .node circle,#mermaid-svg-3pQqYYS7rr7zgW1S .node ellipse,#mermaid-svg-3pQqYYS7rr7zgW1S .node polygon,#mermaid-svg-3pQqYYS7rr7zgW1S .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-3pQqYYS7rr7zgW1S .rough-node .label text,#mermaid-svg-3pQqYYS7rr7zgW1S .node .label text,#mermaid-svg-3pQqYYS7rr7zgW1S .image-shape .label,#mermaid-svg-3pQqYYS7rr7zgW1S .icon-shape .label{text-anchor:middle;}#mermaid-svg-3pQqYYS7rr7zgW1S .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3pQqYYS7rr7zgW1S .rough-node .label,#mermaid-svg-3pQqYYS7rr7zgW1S .node .label,#mermaid-svg-3pQqYYS7rr7zgW1S .image-shape .label,#mermaid-svg-3pQqYYS7rr7zgW1S .icon-shape .label{text-align:center;}#mermaid-svg-3pQqYYS7rr7zgW1S .node.clickable{cursor:pointer;}#mermaid-svg-3pQqYYS7rr7zgW1S .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-3pQqYYS7rr7zgW1S .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-3pQqYYS7rr7zgW1S .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-3pQqYYS7rr7zgW1S .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-3pQqYYS7rr7zgW1S .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-3pQqYYS7rr7zgW1S .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-3pQqYYS7rr7zgW1S .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-3pQqYYS7rr7zgW1S .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-3pQqYYS7rr7zgW1S .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-3pQqYYS7rr7zgW1S .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-3pQqYYS7rr7zgW1S .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-3pQqYYS7rr7zgW1S div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:Microsoft YaHei;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3pQqYYS7rr7zgW1S .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3pQqYYS7rr7zgW1S rect.text{fill:none;stroke-width:0;}#mermaid-svg-3pQqYYS7rr7zgW1S .icon-shape,#mermaid-svg-3pQqYYS7rr7zgW1S .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-3pQqYYS7rr7zgW1S .icon-shape p,#mermaid-svg-3pQqYYS7rr7zgW1S .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-3pQqYYS7rr7zgW1S .icon-shape .label rect,#mermaid-svg-3pQqYYS7rr7zgW1S .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-3pQqYYS7rr7zgW1S .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3pQqYYS7rr7zgW1S .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3pQqYYS7rr7zgW1S :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} MappedFile。
一个 1GB CommitLog 文件。
① fileChannel。
磁盘文件的 NIO 通道。
------ 真正的持久化目标。
② mappedByteBuffer。
mmap 映射区(1GB 虚拟内存)。
------ 不开堆外池时,
写入直接进这里
(等价于写 PageCache)。
③ writeBuffer
(堆外池借的)。
transientStorePoolEnable = true 时,
写入先进这块 DirectByteBuffer。
------ 和 PageCache 完全隔离。
1.2 transientStorePool:为什么要多一块堆外内存
问题 :不开堆外池时,写入直接写 mmap 区(= PageCache)------读写共用 PageCache。消费端大量读冷数据时把 PageCache 刷脏/换出,写入端跟着抖动(写延迟毛刺);反之写入高峰也影响读。
解法(读写分离):
text
写入路径(transientStorePoolEnable=true):
Producer → writeBuffer(堆外 DirectByteBuffer,5 个×1GB 轮转)
→ CommitRealTimeService 定时 commit → FileChannel(进 PageCache)
→ FlushRealTimeService 定时 flush → 磁盘
读取路径:
Consumer → mappedByteBuffer(PageCache)------和写入的 writeBuffer 互不干扰
| 维度 | 不开堆外池 | 开堆外池 |
|---|---|---|
| 写入目标 | mmap 区(PageCache) | 堆外 writeBuffer |
| 读写干扰 | 共用 PageCache,互相抖动 | 读写分离,写入无毛刺 |
| 数据安全性 | 宕机丢 PageCache 未刷盘部分 | 宕机丢 writeBuffer+PageCache 两段(更多!) |
| 适用 | 默认 | 大内存(64G+)+ 主从同步复制(丢的这段 Slave 有) |
关键权衡 :堆外池用"多丢一段内存数据的风险"换"写入延迟稳定"------所以官方要求配合 SYNC_MASTER(同步复制)使用,Slave 兜底那段风险。
1.3 文件预热(warmMappedFile)
新建 1GB MappedFile 后直接写会缺页中断(mmap 只建了虚拟映射,物理页没分配)------每页第一次写都触发缺页,写入抖动。预热做两件事:
java
// MappedFile#warmMappedFile 伪代码
① 逐页写 0:for (i = 0; i < fileSize; i += pageSize) writeBuffer.put(i, (byte)0)
------强制分配物理页,消除缺页中断
② mlock + madvise(WILLNEED):锁定内存不被换出 + 提示内核预读
这就是 transientStorePoolEnable=true 时"新文件创建瞬间写入不抖"的原因------预热在文件创建时一次性做完。
1.4 写入锁的演进:putMessageLock
| 锁实现 | 行为 | 适用 |
|---|---|---|
| ReentrantLock(默认) | 排队加锁------锁竞争激烈时线程挂起/唤醒有开销 | 常规 |
| 自旋锁(CommitLog 高并发) | CAS 自旋------不挂起线程,短临界区更快 | 超高并发写入(useReentrantLockWhenPutMessage=false) |
临界区里做什么 :追加消息到 writeBuffer/mappedByteBuffer + 更新 wrotePosition------纯内存操作,微秒级 ,所以自旋锁划算(临界区短,自旋等待 < 挂起唤醒开销)。这和 09-A 篇"synchronized 偏向锁/轻量级锁"的思想同源:短临界区用自旋,长临界区用挂起。
二、ReputMessageService:索引是怎么异步构建的
2.1 分发流程
#mermaid-svg-7aKQANk3G8uyoZ0Y{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7aKQANk3G8uyoZ0Y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7aKQANk3G8uyoZ0Y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7aKQANk3G8uyoZ0Y .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-7aKQANk3G8uyoZ0Y .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-7aKQANk3G8uyoZ0Y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7aKQANk3G8uyoZ0Y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7aKQANk3G8uyoZ0Y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7aKQANk3G8uyoZ0Y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7aKQANk3G8uyoZ0Y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7aKQANk3G8uyoZ0Y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7aKQANk3G8uyoZ0Y .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-7aKQANk3G8uyoZ0Y .marker.cross{stroke:#0b0b0b;}#mermaid-svg-7aKQANk3G8uyoZ0Y svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-7aKQANk3G8uyoZ0Y p{margin:0;}#mermaid-svg-7aKQANk3G8uyoZ0Y .label{font-family:Microsoft YaHei;color:#333;}#mermaid-svg-7aKQANk3G8uyoZ0Y .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-7aKQANk3G8uyoZ0Y .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-7aKQANk3G8uyoZ0Y .cluster-label span p{background-color:transparent;}#mermaid-svg-7aKQANk3G8uyoZ0Y .label text,#mermaid-svg-7aKQANk3G8uyoZ0Y span{fill:#333;color:#333;}#mermaid-svg-7aKQANk3G8uyoZ0Y .node rect,#mermaid-svg-7aKQANk3G8uyoZ0Y .node circle,#mermaid-svg-7aKQANk3G8uyoZ0Y .node ellipse,#mermaid-svg-7aKQANk3G8uyoZ0Y .node polygon,#mermaid-svg-7aKQANk3G8uyoZ0Y .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-7aKQANk3G8uyoZ0Y .rough-node .label text,#mermaid-svg-7aKQANk3G8uyoZ0Y .node .label text,#mermaid-svg-7aKQANk3G8uyoZ0Y .image-shape .label,#mermaid-svg-7aKQANk3G8uyoZ0Y .icon-shape .label{text-anchor:middle;}#mermaid-svg-7aKQANk3G8uyoZ0Y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7aKQANk3G8uyoZ0Y .rough-node .label,#mermaid-svg-7aKQANk3G8uyoZ0Y .node .label,#mermaid-svg-7aKQANk3G8uyoZ0Y .image-shape .label,#mermaid-svg-7aKQANk3G8uyoZ0Y .icon-shape .label{text-align:center;}#mermaid-svg-7aKQANk3G8uyoZ0Y .node.clickable{cursor:pointer;}#mermaid-svg-7aKQANk3G8uyoZ0Y .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-7aKQANk3G8uyoZ0Y .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-7aKQANk3G8uyoZ0Y .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-7aKQANk3G8uyoZ0Y .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-7aKQANk3G8uyoZ0Y .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-7aKQANk3G8uyoZ0Y .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-7aKQANk3G8uyoZ0Y .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-7aKQANk3G8uyoZ0Y .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-7aKQANk3G8uyoZ0Y .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-7aKQANk3G8uyoZ0Y .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-7aKQANk3G8uyoZ0Y .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-7aKQANk3G8uyoZ0Y div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:Microsoft YaHei;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7aKQANk3G8uyoZ0Y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7aKQANk3G8uyoZ0Y rect.text{fill:none;stroke-width:0;}#mermaid-svg-7aKQANk3G8uyoZ0Y .icon-shape,#mermaid-svg-7aKQANk3G8uyoZ0Y .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-7aKQANk3G8uyoZ0Y .icon-shape p,#mermaid-svg-7aKQANk3G8uyoZ0Y .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-7aKQANk3G8uyoZ0Y .icon-shape .label rect,#mermaid-svg-7aKQANk3G8uyoZ0Y .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-7aKQANk3G8uyoZ0Y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7aKQANk3G8uyoZ0Y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7aKQANk3G8uyoZ0Y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}
消息写入 CommitLog 成功。
wrotePosition 前进。
ReputMessageService 线程。
每 1ms 轮询:
从 reputFromOffset 开始,
读 CommitLog 里的新消息。
doReput:
逐条解析消息头。
DispatcherBuildConsumeQueue。
→ 写 ConsumeQueue,20 字节 / 条。
DispatcherBuildIndex。
→ 写 IndexFile,Key 哈希槽。
reputFromOffset 前进,
继续下一批。
2.2 三个源码级结论
- 索引构建是异步的 ------消息写入 CommitLog 成功即返回 Producer,ConsumeQueue/IndexFile 由 ReputMessageService 事后构建 (毫秒级延迟)。所以:发送成功 ≠ 立即可消费 (有 reput 延迟);按 Key 查刚发的消息可能查不到(IndexFile 还没建)。
- reputFromOffset 是"索引进度" ------存在 config 里,Broker 重启后从这里继续构建;ConsumeQueue 损坏可以全量重建(从 CommitLog 重新 reput)------CommitLog 是唯一事实源。
- 分发器是可扩展点 ------CommitLogDispatcher 接口允许挂自定义分发器(如 5.x 的 TimerMessageStore 延迟消息、事务消息的 op 处理都是分发器)------"一次写入,多方消费日志"是 RocketMQ 内部特性的统一实现范式(和 Kafka 的 LogSegment 多消费者思路一致)。
三、长轮询:PullRequestHoldService 的挂起与唤醒
3.1 为什么长轮询是"伪 Push"
02 篇说过 Push 本质是长轮询------源码层面:
ReputMessageService PullRequestHoldService Broker(PullMessageProcessor) Consumer(PullMessageService) ReputMessageService PullRequestHoldService Broker(PullMessageProcessor) Consumer(PullMessageService) #mermaid-svg-bUH9hzDHuqoyoTnX{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bUH9hzDHuqoyoTnX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bUH9hzDHuqoyoTnX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bUH9hzDHuqoyoTnX .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-bUH9hzDHuqoyoTnX .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-bUH9hzDHuqoyoTnX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bUH9hzDHuqoyoTnX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bUH9hzDHuqoyoTnX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bUH9hzDHuqoyoTnX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bUH9hzDHuqoyoTnX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bUH9hzDHuqoyoTnX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bUH9hzDHuqoyoTnX .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-bUH9hzDHuqoyoTnX .marker.cross{stroke:#0b0b0b;}#mermaid-svg-bUH9hzDHuqoyoTnX svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-bUH9hzDHuqoyoTnX p{margin:0;}#mermaid-svg-bUH9hzDHuqoyoTnX .actor{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-bUH9hzDHuqoyoTnX text.actor>tspan{fill:#333;stroke:none;}#mermaid-svg-bUH9hzDHuqoyoTnX .actor-line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);}#mermaid-svg-bUH9hzDHuqoyoTnX .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-bUH9hzDHuqoyoTnX .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-bUH9hzDHuqoyoTnX .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-bUH9hzDHuqoyoTnX #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-bUH9hzDHuqoyoTnX .sequenceNumber{fill:#f4f4f4;}#mermaid-svg-bUH9hzDHuqoyoTnX #sequencenumber{fill:#333;}#mermaid-svg-bUH9hzDHuqoyoTnX #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-bUH9hzDHuqoyoTnX .messageText{fill:#333;stroke:none;}#mermaid-svg-bUH9hzDHuqoyoTnX .labelBox{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-bUH9hzDHuqoyoTnX .labelText,#mermaid-svg-bUH9hzDHuqoyoTnX .labelText>tspan{fill:#333;stroke:none;}#mermaid-svg-bUH9hzDHuqoyoTnX .loopText,#mermaid-svg-bUH9hzDHuqoyoTnX .loopText>tspan{fill:#333;stroke:none;}#mermaid-svg-bUH9hzDHuqoyoTnX .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:hsl(40.5882352941, 60%, 83.3333333333%);}#mermaid-svg-bUH9hzDHuqoyoTnX .note{stroke:hsl(52.6829268293, 60%, 73.9215686275%);fill:#fff5ad;}#mermaid-svg-bUH9hzDHuqoyoTnX .noteText,#mermaid-svg-bUH9hzDHuqoyoTnX .noteText>tspan{fill:#333;stroke:none;}#mermaid-svg-bUH9hzDHuqoyoTnX .activation0{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-bUH9hzDHuqoyoTnX .activation1{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-bUH9hzDHuqoyoTnX .activation2{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-bUH9hzDHuqoyoTnX .actorPopupMenu{position:absolute;}#mermaid-svg-bUH9hzDHuqoyoTnX .actorPopupMenuPanel{position:absolute;fill:#fff4dd;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-bUH9hzDHuqoyoTnX .actor-man line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-bUH9hzDHuqoyoTnX .actor-man circle,#mermaid-svg-bUH9hzDHuqoyoTnX line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;stroke-width:2px;}#mermaid-svg-bUH9hzDHuqoyoTnX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 每 5s 检查一遍挂起请求 par 兜底轮询 事件唤醒(毫秒级投递延迟的来源) 超时(15s)仍无消息 → 返回"没消息" Consumer 立即再发起下一次拉取(长轮询闭环) alt 有消息 没消息 且 长轮询开启(brokerSuspendMaxTimeMillis\>0) PULL_MESSAGE 拉取请求 1 立即返回消息 2 包装成 PullRequest 挂起(不返回!连接 hold 住,最长 15s) 3 该 Queue 有新消息(maxOffset > 请求 offset)? 4 有消息 → 立即执行拉取并返回 5 新消息 reput 到 ConsumeQueue 后 notifyMessageArriving 6 立即唤醒对应 Queue 的挂起请求并返回 7
3.2 关键参数对照
| 参数 | 默认 | 含义 |
|---|---|---|
| longPollingEnable | true | 开启长轮询(false 退化为短轮询,1s 挂起) |
| brokerSuspendMaxTimeMillis | 15s | Broker 端 hold 请求的最长时间 |
| consumerTimeoutMillisWhenSuspend | 30s | Consumer 端等待超时(必须 > Broker 的 15s,否则 Consumer 先超时误判) |
| pullInterval | 0 | Consumer 两次拉取间隔------0 = 收到响应立即再拉(长轮询闭环) |
一句话 :长轮询 = "请求挂起 + 新消息事件唤醒 + 超时兜底"三件套------实时性来自 notifyMessageArriving 的事件唤醒,不是傻等 5s 轮询。
四、刷盘服务与崩溃恢复
4.1 两种刷盘服务的线程模型
| 服务 | 触发 | 行为 |
|---|---|---|
| GroupCommitService(同步刷盘) | Producer 请求 SYNC_FLUSH | 写线程把 GroupCommitRequest 放入队列 → 刷盘线程批量刷(每 10ms 一批)→ 写线程在 request.future 上等待(最多 5s)→ 刷完唤醒返回 |
| FlushRealTimeService(异步刷盘) | 定时(500ms) | 后台把 PageCache 刷盘,写线程不等待 |
同步刷盘也不是每条刷一次 ------GroupCommit 把 10ms 内的请求合并成一批刷 (组提交,和 MySQL redo log 组提交、Kafka 批量发送同一个思想:攒批摊薄 IO 开销)。
4.2 崩溃恢复流程
#mermaid-svg-32heavBI0VxpfQze{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-32heavBI0VxpfQze .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-32heavBI0VxpfQze .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-32heavBI0VxpfQze .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-32heavBI0VxpfQze .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-32heavBI0VxpfQze .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-32heavBI0VxpfQze .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-32heavBI0VxpfQze .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-32heavBI0VxpfQze .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-32heavBI0VxpfQze .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-32heavBI0VxpfQze .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-32heavBI0VxpfQze .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-32heavBI0VxpfQze .marker.cross{stroke:#0b0b0b;}#mermaid-svg-32heavBI0VxpfQze svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-32heavBI0VxpfQze p{margin:0;}#mermaid-svg-32heavBI0VxpfQze .label{font-family:Microsoft YaHei;color:#333;}#mermaid-svg-32heavBI0VxpfQze .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-32heavBI0VxpfQze .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-32heavBI0VxpfQze .cluster-label span p{background-color:transparent;}#mermaid-svg-32heavBI0VxpfQze .label text,#mermaid-svg-32heavBI0VxpfQze span{fill:#333;color:#333;}#mermaid-svg-32heavBI0VxpfQze .node rect,#mermaid-svg-32heavBI0VxpfQze .node circle,#mermaid-svg-32heavBI0VxpfQze .node ellipse,#mermaid-svg-32heavBI0VxpfQze .node polygon,#mermaid-svg-32heavBI0VxpfQze .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-32heavBI0VxpfQze .rough-node .label text,#mermaid-svg-32heavBI0VxpfQze .node .label text,#mermaid-svg-32heavBI0VxpfQze .image-shape .label,#mermaid-svg-32heavBI0VxpfQze .icon-shape .label{text-anchor:middle;}#mermaid-svg-32heavBI0VxpfQze .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-32heavBI0VxpfQze .rough-node .label,#mermaid-svg-32heavBI0VxpfQze .node .label,#mermaid-svg-32heavBI0VxpfQze .image-shape .label,#mermaid-svg-32heavBI0VxpfQze .icon-shape .label{text-align:center;}#mermaid-svg-32heavBI0VxpfQze .node.clickable{cursor:pointer;}#mermaid-svg-32heavBI0VxpfQze .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-32heavBI0VxpfQze .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-32heavBI0VxpfQze .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-32heavBI0VxpfQze .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-32heavBI0VxpfQze .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-32heavBI0VxpfQze .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-32heavBI0VxpfQze .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-32heavBI0VxpfQze .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-32heavBI0VxpfQze .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-32heavBI0VxpfQze .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-32heavBI0VxpfQze .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-32heavBI0VxpfQze div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:Microsoft YaHei;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-32heavBI0VxpfQze .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-32heavBI0VxpfQze rect.text{fill:none;stroke-width:0;}#mermaid-svg-32heavBI0VxpfQze .icon-shape,#mermaid-svg-32heavBI0VxpfQze .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-32heavBI0VxpfQze .icon-shape p,#mermaid-svg-32heavBI0VxpfQze .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-32heavBI0VxpfQze .icon-shape .label rect,#mermaid-svg-32heavBI0VxpfQze .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-32heavBI0VxpfQze .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-32heavBI0VxpfQze .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-32heavBI0VxpfQze :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不存在(正常关闭)
存在(上次异常宕机)
Broker 启动。
abort 文件存在?
不存在(正常关闭)→
正常恢复:
只校验最后一个文件。
快,秒级。
存在(上次异常宕机)→
完整恢复:
从倒数第 3 个文件开始,
逐个校验。
校验每条消息:
magic 魔数 + CRC + 存储时间
(> 30s 前的才算可信)。
找到最后一个「完整文件」的位置。
processOffset = 可信数据的末尾。
截断:
mappedFile 的 wrotePosition
回退到 processOffset。
------ 丢弃宕机时的半截数据。
重建 ConsumeQueue / IndexFile。
从 CommitLog reput。
删除 abort 文件,启动完成。
三个设计细节:
- abort 哨兵文件 :启动时创建、shutdown hook 删除------"文件还在 = 上次没善终",比读日志判断快且可靠(和 HBase 的 WAL 恢复触发条件、Redis 的 RDB/AOF 选择同款思想);
- 从倒数第 3 个文件开始 :宕机时正在写的文件 + 可能受 PageCache 回写影响的前 2 个------3 个文件的窗口覆盖脏数据范围,不用全量扫描(TB 级数据分钟级恢复);
- CRC + 时间双重校验 :CRC 防位翻转,"存储时间距当前 >30s 才可信"防时钟回拨/文件错乱------校验不过的位置就是截断点。
4.3 ConsumeQueue 损坏的救法
ConsumeQueue 只是索引(20 字节定长条目),损坏不丢数据:
shell
# 删除损坏的 ConsumeQueue 目录,Broker 重启后从 CommitLog 全量 reput 重建
rm -rf ~/store/consumequeue/order-topic
# 重启 Broker → ReputMessageService 从 reputFromOffset=0(或 checkpoint)重建
# 代价:重建期间该 Topic 不可消费;TB 级 CommitLog 重建要小时级
这就是"CommitLog 是唯一事实源"的运维含义------索引可弃可重建,数据文件才是命根子。
五、5.x 新存储特性:Pop 消费与分层存储
5.1 Pop 消费:无状态消费模式
Push 模式的痛点 :Queue 被 Consumer 独占(Rebalance 分配)------Consumer 数量受 Queue 数限制、Rebalance 抖动、位点管理在 Consumer 侧复杂。
Pop 模式(5.x):
| 维度 | Push(经典) | Pop(5.x) |
|---|---|---|
| Queue 归属 | Rebalance 独占分配 | 无归属,谁 pop 谁消费(Broker 端负载均衡) |
| 位点管理 | Consumer 提交 offset | Broker 管"不可见时间"(pop 后消息隐藏 N 秒,ack 则删标记,超时未 ack 自动重投) |
| 扩容 | Consumer ≤ Queue 数 | Consumer 数不受 Queue 限制 |
| 类比 | Kafka Consumer Group | SQS/RocketMQ Lite Topic 的" invisibility timeout"模型 |
本质 :Pop 把"队列独占+位点"模型换成"原子弹出+租约超时"模型------和 Redis 分布式锁的租约、SQS 的 VisibilityTimeout 是同一个思想(15-A 篇)。
5.2 分层存储(Tiered Storage)
text
热数据(最近 72h):本地 SSD CommitLog ------ 毫秒级读写
冷数据(>72h):自动上传对象存储(OSS/S3/HDFS)------ 本地只留元数据
读取冷数据:Broker 代理从对象存储拉取 ------ 延迟高但可回溯
价值 :消息保留期从"磁盘容量决定"变成"对象存储成本决定"------保留 30 天的成本降到 1/10 (对象存储单价 ≈ SSD 的 1/5,且不用副本×3)。和 ES 冷热分离(17-A 篇)、HBase 冷数据下沉是同一个架构模式:按访问温度分层,热层保性能,冷层保成本。
六、跑一遍:观察堆外池与恢复行为
6.1 查看存储层运行时状态
shell
# ① 查看 Broker 存储统计(CommitLog 位置、reput 进度、刷盘状态)
sh bin/mqadmin brokerStatus -n localhost:9876 -b localhost:10911 | grep -E "commitLog|reput|flush|PageCache"
① 的关键输出:
text
#putMessageDistributeTime = [<=0ms]:120344 [0~10ms]:892 [10~100ms]:3 ...
# 写入耗时分布:99.3% 在 0ms 内(PageCache/堆外池命中),极少数 10ms+(刷盘毛刺)
commitLogDiskRatio = 0.31 # 磁盘使用率
commitLogMaxOffset = 1099511627776 # CommitLog 写到哪了
reputFromOffset = 1099511627776 # 索引构建进度(= maxOffset 说明索引追平)
flushDiskType = ASYNC_FLUSH
transientStorePoolEnable = true # 堆外池开启
pageCacheLockTimeMills = 0 # PageCache 锁耗时(0=健康)
观察点 :reputFromOffset == commitLogMaxOffset 说明索引构建追平写入------两者差值持续增大 = ReputMessageService 跟不上(磁盘慢/消息体大),消费会延迟。
6.2 模拟异常宕机观察恢复
shell
# ② 持续写入后,kill -9 强杀 Broker(模拟断电,abort 文件残留)
sh bin/benchmark.sh -n localhost:9876 -t recover-test -w 4 &
kill -9 $(pgrep -f DefaultBrokerStartup)
# ③ 确认 abort 文件存在
ls ~/store/abort
# 输出:/root/store/abort ← 哨兵文件还在 = 异常宕机
# ④ 重启 Broker,观察恢复日志
sh bin/mqbroker -n localhost:9876 &
grep -E "recover|abort" ~/logs/rocketmqlogs/store.log
④ 的恢复日志(走完整恢复流程):
text
load over, ... abort file exists, do recover normally=false ← 检测到 abort,完整恢复
recover topicConfig ... begin
recover next physical file, ... ← 从倒数第 3 个文件开始校验
recover physical file end, processOffset=1073741824 ← 找到可信数据末尾
ConsumeQueue recover end, ... ← ConsumeQueue 重建完成
recover end, total 2 physical file, cost 4321 ms ← 恢复耗时 4.3s
💡 对照理解 :③的 abort 文件就是 4.2 节的哨兵------正常
mqshutdown broker会删掉它 (走快速恢复);kill -9删不掉(走完整恢复)。④日志里的processOffset就是截断点------宕机瞬间 PageCache 里没刷盘的"半截数据"在这里被丢弃,这就是异步刷盘"可能丢"的确切含义:丢的是 processOffset 之后、Slave 也没同步到的那一小段(同步复制下 Slave 有,所以不丢)。
七、总结
7.1 一张图回顾全文
#mermaid-svg-zdvZC0WWVYcy8Iz2{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zdvZC0WWVYcy8Iz2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-zdvZC0WWVYcy8Iz2 .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-zdvZC0WWVYcy8Iz2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .marker.cross{stroke:#0b0b0b;}#mermaid-svg-zdvZC0WWVYcy8Iz2 svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-zdvZC0WWVYcy8Iz2 p{margin:0;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .label{font-family:Microsoft YaHei;color:#333;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-zdvZC0WWVYcy8Iz2 .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-zdvZC0WWVYcy8Iz2 .cluster-label span p{background-color:transparent;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .label text,#mermaid-svg-zdvZC0WWVYcy8Iz2 span{fill:#333;color:#333;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .node rect,#mermaid-svg-zdvZC0WWVYcy8Iz2 .node circle,#mermaid-svg-zdvZC0WWVYcy8Iz2 .node ellipse,#mermaid-svg-zdvZC0WWVYcy8Iz2 .node polygon,#mermaid-svg-zdvZC0WWVYcy8Iz2 .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .rough-node .label text,#mermaid-svg-zdvZC0WWVYcy8Iz2 .node .label text,#mermaid-svg-zdvZC0WWVYcy8Iz2 .image-shape .label,#mermaid-svg-zdvZC0WWVYcy8Iz2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .rough-node .label,#mermaid-svg-zdvZC0WWVYcy8Iz2 .node .label,#mermaid-svg-zdvZC0WWVYcy8Iz2 .image-shape .label,#mermaid-svg-zdvZC0WWVYcy8Iz2 .icon-shape .label{text-align:center;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .node.clickable{cursor:pointer;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-zdvZC0WWVYcy8Iz2 .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-zdvZC0WWVYcy8Iz2 .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-zdvZC0WWVYcy8Iz2 .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-zdvZC0WWVYcy8Iz2 .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-zdvZC0WWVYcy8Iz2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:Microsoft YaHei;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zdvZC0WWVYcy8Iz2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .icon-shape,#mermaid-svg-zdvZC0WWVYcy8Iz2 .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .icon-shape p,#mermaid-svg-zdvZC0WWVYcy8Iz2 .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .icon-shape .label rect,#mermaid-svg-zdvZC0WWVYcy8Iz2 .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-zdvZC0WWVYcy8Iz2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zdvZC0WWVYcy8Iz2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zdvZC0WWVYcy8Iz2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RocketMQ 存储深水区。
写入路径。
MappedFile 三块内存:
fileChannel / mmap 区 / 堆外 writeBuffer。
transientStorePool 读写分离,
多丢一段换延迟稳定,配 SYNC_MASTER。
预热消除缺页中断。
短临界区用自旋锁。
索引与长轮询。
ReputMessageService 异步构建索引。
发送成功 ≠ 立即可消费。
CommitLog 是唯一事实源。
长轮询 = 挂起 + 事件唤醒 + 超时兜底。
刷盘与恢复。
同步刷盘 = 组提交(10ms 攒批)。
abort 哨兵判断正常 / 异常关闭。
倒数第 3 个文件起 CRC + 时间校验。
截断半截数据 + 重建 ConsumeQueue。
5.x 新特性。
Pop 消费:无状态 + 不可见时间租约。
Consumer 数不再受 Queue 限制。
分层存储:冷数据下沉对象存储。
保留期成本降 10 倍。
7.2 核心要点浓缩(十二条)
- MappedFile 三块内存 :fileChannel(持久化目标)、mappedByteBuffer(mmap 区)、writeBuffer(堆外池)------堆外池开关决定写入进哪块。
- transientStorePool 读写分离 :写堆外池、读 PageCache,互不干扰------代价是宕机多丢一段(writeBuffer),必须配 SYNC_MASTER 由 Slave 兜底。
- commit/flush 两段式 :堆外→FileChannel(commit)→磁盘(flush)------比直接 mmap 多一跳,换延迟稳定。
- 文件预热 :逐页写 0 分配物理页 + mlock 锁定------消除新文件缺页中断毛刺。
- 写入锁 :短临界区(纯内存追加)用自旋锁比 ReentrantLock 快------和 synchronized 轻量级锁同款思想。
- ReputMessageService :1ms 轮询 CommitLog 异步构建 ConsumeQueue/IndexFile------发送成功≠立即可消费;按 Key 查刚发的消息可能查不到。
- CommitLogDispatcher 扩展点 :延迟消息/事务消息/Pop 都是分发器------"一次写入,多方消费日志"是内部特性的统一范式。
- 长轮询三件套 :请求挂起(HoldService)+ 新消息事件唤醒(notifyMessageArriving)+ 15s 超时兜底------毫秒级投递延迟来自事件唤醒,不是轮询。
- 同步刷盘=组提交 :10ms 攒批刷------和 MySQL redo 组提交、Kafka 批量发送同一个"攒批摊薄 IO"思想。
- 崩溃恢复 :abort 哨兵判断异常宕机 → 倒数第 3 个文件起 CRC+时间校验 → 截断 processOffset 后半截数据 → 重建索引------TB 级分钟恢复,不用全量扫。
- Pop 消费(5.x) :无状态消费+不可见时间租约------Consumer 数不再受 Queue 数限制,Broker 端负载均衡(SQS 模型)。
- 分层存储(5.x) :冷数据下沉对象存储------保留期成本降 10 倍,和 ES 冷热分离同款架构模式。
📌 最后一句话 :RocketMQ 存储深水区的三个统一范式------"攒批" (组提交刷盘/批量 reput/长轮询唤醒)、"分离" (读写分离的堆外池/写读分离的 CommitLog-ConsumeQueue)、"日志为王" (CommitLog 是唯一事实源,索引可弃可重建,特性皆分发器)。看懂这三个范式,5.x 的 Pop 和分层存储就只是范式在新场景的复用------源码会过时,范式不会。
📌 配套阅读:
上一篇:《06-03-A-RocketMQ高可用与生产调优详解.md》
如果这篇文章对你有帮助,欢迎点赞、收藏、关注!