存储机制
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、零拷贝。