RocketMQ 存储机制

存储机制

RocketMQ 能扛高吞吐,很大程度靠存储层设计。核心是三个文件:

文件 作用 类比
CommitLog 所有消息的主体,顺序写入 一本书的正文
ConsumeQueue 每个 Queue 的索引,指向 CommitLog 书的目录
IndexFile 按 Key / 时间查消息的索引 书的索引页

一、整体结构

vbnet 复制代码
        ┌──────────────────────────────────────────┐
        │               CommitLog                  │
        │  所有消息顺序写入,默认每个文件 1GB        │
        │ (消息按到达顺序混在一起,不分 Topic)      │
        └──────────────────────────────────────────┘
             ▲                              ▲
             │ 记录:偏移量 + 长度 + Tag哈希   │ 记录:Key → 偏移量
             │                              │
   ┌─────────┴──────────┐        ┌──────────┴─────────┐
   │    ConsumeQueue    │        │     IndexFile      │
   │  每个 Queue 一个    │        │  按 Key / 时间 查询 │
   │  相当于「二级索引」  │        │  (辅助,可选)      │
   └────────────────────┘        └────────────────────┘

关键理解 :消息只存一份 (在 CommitLog),ConsumeQueue 和 IndexFile 都只是索引,存的是「去哪找」。


二、CommitLog(消息主体)

是什么 :所有消息顺序写入的日志文件,默认每个文件 1GB。

它有什么用?

一句话 :它是消息真正存放的地方------ConsumeQueue 和 IndexFile 都只是索引,存的是「去哪找」。

作用 说明
存消息本体 消息的 Topic、Tag、Body 全都只在这里
写入快 所有消息统一写一处、只追加不修改 → 磁盘顺序写
不丢消息 落盘后 Broker 宕机重启还能恢复
支持回溯 保留期内(默认 72 小时)可重置 offset 重新消费

一句话记:CommitLog 是「原始数据」,另外两个是「衍生数据」------它删了消息就没了,ConsumeQueue 删了还能重建。

它是 MQ 自带的吗?

不是。Broker 运行时自己创建的。

  • 启动 Broker 后,它会在磁盘上建一个存储目录(默认 ~/store)
  • 消息来了就自己创建文件往里写
  • 不用手动建,也不用管

类比:像 MySQL 的 .ibd 数据文件------不是安装包里的,是跑起来之后自己生成的。

「写满滚动到下一个」操作

CommitLog 不是「一个文件」,而是「一串文件」。 一个文件写到 1GB 了,Broker 就新建一个接着写。

磁盘上长这样:

bash 复制代码
store/commitlog/
  00000000000000000000      ← 第 1 个,1GB
  0000000000001073741824    ← 第 2 个,1GB
  0000000000002147483648    ← 第 3 个,1GB

文件名不是随便编的,而是「这个文件从第几个字节开始」:

文件 起始字节 为什么是这个数
00000000000000000000 0 第 1 个文件
0000000000001073741824 1073741824 = 1GB(上一个写满了)
0000000000002147483648 2147483648 = 2GB

为什么要切成多个文件:

原因 说明
好删除 消息有保留期(默认 72 小时),过期了直接删整个文件,不用去文件中间挖
好管理 单个文件太大不好操作(映射一个 100GB 的文件很麻烦)
好定位 文件名就是起始偏移量,给一个绝对偏移量,一除就知道在哪个文件、文件内什么位置

三个特点:

  • 只有一份 :不管哪个 Topic 的消息,都写进同一串 CommitLog 文件里(不像 ConsumeQueue,是每个 Queue 一个索引文件)
  • 顺序写:只追加(append),从不修改已有内容
  • 不定长:每条消息长度不一样,所以不能直接按位置算偏移

为什么要顺序写:

写入方式 速度
磁盘顺序写 接近内存随机写的速度
磁盘随机写 慢一个数量级

一句话:CommitLog 用「只追加、不修改」换来了磁盘顺序写的高性能,这是 RocketMQ 高吞吐的根基。


三、ConsumeQueue(消费索引)

是什么 :CommitLog 中每条消息的索引文件

解决什么问题 :CommitLog 里的消息是按到达顺序混着存的 ,而且长度不一 。如果消费者直接扫 CommitLog 找自己 Topic 的消息,得从头扫到尾,非常低效。 用法:Consumer(消费者)通过消费 ConsumeQueue 来定位消息的偏移量,然后再去 CommitLog 中按偏移量读取具体的消息内容

做法 :给每个 Topic + Queue 建一个索引文件。

每条记录固定 20 字节:

复制代码
┌──────────────┬────────────┬──────────────┐
│   8 字节      │   4 字节    │    8 字节     │
│  CommitLog   │  消息长度   │  Tag 哈希值   │
│   偏移量      │            │              │
└──────────────┴────────────┴──────────────┘

三个字段的用途:

字段 用途
CommitLog 偏移量 去 CommitLog 哪个位置读消息
消息长度 读多长
Tag(分类标签,类似于二级主题) 哈希值 消息过滤------先按哈希筛掉不匹配的,再回 CommitLog 读真实内容

为什么是定长 20 字节 :定长意味着可以直接算出 第 N 条记录的位置(起始位置 + N × 20),不用遍历。这就是「二级索引」的价值。

一句话:ConsumeQueue 把「不定长的消息流」变成了「定长的索引数组」,让消费者能直接定位,定位后再从CommitLog 读数据。


四、IndexFile(按 Key / 时间查消息)

是什么 :按消息 Key 或时间范围定位到 CommitLog 偏移量的索引文件。

三个关键点

问题 答案
谁创建 Broker 自动创建,启动时预先建好空文件;写满 1GB 滚动到下一个(文件名用时间戳)
什么时候写 只有消息设了 Key(setKeys)才写;没设 Key 的消息不进 IndexFile
谁在用 运维排查 和 Console 搜索。消费者不走它

类比:像图书馆的「按 ISBN 查书」目录------只有分配了 ISBN 的书才登记,没 ISBN 的不进这个目录。

怎么查(两步)

vbnet 复制代码
① 定位:按 Key 查哈希表(或用时间范围,扫对应时间段的 IndexFile)
② 取值:拿到 CommitLog 偏移量 → 回 CommitLog 读完整消息

和 ConsumeQueue 的区别

ConsumeQueue IndexFile
索引维度 Topic + Queue Key(哈希)
消费者走吗 走(必经之路) 不走
是否必有 必有 可选(没设 Key 就没有)

常问

问 答
什么时候会用到它? 只有消息设了 Key 才构建;日常消费走 ConsumeQueue,它是排查用的辅助索引
和 ConsumeQueue 的区别? ConsumeQueue 按 Topic + Queue,是消费者必经之路;IndexFile 按 Key 做哈希索引,只在查消息时用
Key 一般设什么? 业务唯一标识,比如订单号 ORD-20261001-001、流水号 PAY-20261001-002
满了怎么办? 写满 1GB 自动切下一个,文件名用时间戳;过期后跟 CommitLog 一起按保留期清理

一句话 :IndexFile 是 Broker 自动维护的稀疏哈希索引 ,只有设了 Key 的消息才写入,供运维排查 用,消费者日常不经过它。


五、消息过滤

消费者订阅 Topic 时,可以只收其中一部分消息。两种方式:

方式 怎么做 优点 缺点
Tag 过滤(推荐) 发消息时打 Tag,订阅时指定 Tag 快------在 ConsumeQueue 层就能筛掉,不用读消息体 一个消息只能有一个 Tag,表达能力有限
SQL92 过滤 按消息属性写 SQL 表达式 灵活,支持复杂条件 慢------要先读出消息体再判断

Tag 过滤为什么快 :因为 Tag 哈希值就存在 ConsumeQueue 的 20 字节里,不用回 CommitLog 读消息体就能筛掉大部分。

选型:能用 Tag 就用 Tag;条件复杂到 Tag 表达不了,再用 SQL92。

一句话:消费端订阅的时候可以指定 Tag,只收自己关心的那类消息


六、高性能原理

手段 说明
顺序写磁盘 CommitLog 只追加不修改,磁盘顺序写速度接近内存随机写
PageCache 操作系统把最近读写的文件页缓存在内存,读消息时多数直接命中内存,不走磁盘
mmap(内存映射) CommitLog 读写用内存映射,写内存就等于写文件,省掉用户态和内核态之间的拷贝
零拷贝 网络发送时数据从 PageCache 直接送到网卡,减少一次用户态拷贝
批量发送与压缩 一次发多条、压缩后发,减少网络开销

顺序写为什么快(一个好记的类比)

顺序写 = 在笔记本上一直往后写 随机写 = 每次翻回中间某页去改一个字

前者快得多。

「RocketMQ 为什么快?」

(三句话串起四个点):

主要是存储设计。消息统一顺序写入 CommitLog,是磁盘顺序写 ,速度接近内存随机写;读的时候走 PageCache ,多数能命中内存;网络传输还用零拷贝减少拷贝次数。再加上 ConsumeQueue 是定长索引,消费者不用扫全量消息就能直接定位。

四个点拆开看:

点 一句话
顺序写 CommitLog 只追加不修改,磁盘顺序写接近内存随机写
PageCache 读消息时多数命中内存,不走磁盘
零拷贝 数据少拷一次,传输更快
定长索引 ConsumeQueue 每条固定 20 字节,一算就定位

注意 :这四个点里,顺序写和 PageCache 是必须能清晰说出;零拷贝和定长索引算加分项。

顺带说下刷盘机制

消息写到 PageCache 之后,什么时候真正落盘?两种策略:

策略 做法 特点
异步刷盘(默认) 后台线程定期批量刷盘 快,但宕机可能丢少量消息
同步刷盘 每条消息都刷盘后才返回成功 慢,但不丢

生产建议 :一般用异步刷盘 + 主从复制兜底;对可靠性要求极高的场景(金融)才用同步刷盘。 一句话:Broker 端默认是异步刷盘,性能好,但极端情况下可能丢少量消息;要保证不丢可以配同步刷盘,或者靠主从复制兜底。


七、总结

  • CommitLog :所有消息顺序写在这,只有一份,默认 1GB 一个文件。
  • ConsumeQueue :每个 Queue 一个索引,每条记录定长 20 字节(偏移量 + 长度 + Tag 哈希),相当于二级索引。
  • IndexFile :按 Key / 时间查消息,排查问题用,不在主链路。
  • 消息过滤:Tag 过滤快(在 ConsumeQueue 层就筛掉),SQL92 灵活但慢。
  • 高性能靠四点:顺序写、PageCache、mmap、零拷贝。
相关推荐
montEvergreen1 小时前
RTMP 王国的“信笺百科全书”
后端·go
北冥you鱼1 小时前
Go 语言 Channel 机制详解:从原理到实战
开发语言·后端·golang
谢亮_vipxieliang1 小时前
Go 并发控制进阶:sync 包高级原语与 Context 生命周期管理
开发语言·后端·golang
用户9479135811622 小时前
ReAct 到底在循环什么:从零拆解 Agent 的「想一步、做一步」机制
后端
TechLee2 小时前
Go 泛型统一 API 响应设计的最佳实践
后端·架构·go
晴天小庭2 小时前
Sael——基于Jev的AI中转站的安全风控网关,现已开源
人工智能·后端·react.js
知守观2 小时前
一个半天需求干了三天:代码腐化的五个信号与自查命令
java·后端·代码规范
过客123452 小时前
从"发现"到"处置":一个无人值守 AI 闭环的完整拆解(85 天真实数据)
后端·agent·ai编程
黎燃2 小时前
我搭了个“大模型辩论赛“:让 DeepSeek、Qwen、GLM 在蓝耘上吵了一架
后端