1. RocksDB 是什么?
TiDB、Flink、Kafka Streams、CockroachDB,这些系统的存储层都用到了同一个东西:RocksDB。
但它不是一个完整的数据库,更像是一个嵌入式存储引擎库——你把它链接到自己的程序里,就能直接存取 KV 数据,不用单独部署服务。它不提供 SQL 解析、网络服务、用户认证等功能,只专注做好单机 KV 持久化。
这篇文章聊聊 RocksDB 是怎么工作的,以及为什么这么多大厂选它。
2. RocksDB 从哪来
2012 年,Facebook 从 LevelDB fork 出了 RocksDB。
它不是数据库,是一个存储引擎库。跟 MySQL 对比一下就明白了:
- MySQL 要部署服务、配置连接、维护进程
- RocksDB 直接链接到你的程序里,调几个函数就能读写
什么时候会用到它?
- 你的程序需要存数据,但不想依赖外部数据库
- 数据量大,写入频繁
- 只需要简单的 KV 接口,不需要 SQL
- 作为分布式系统的存储底座(TiDB 就是这么用的)
注意:RocksDB 运行在调用者进程内。如果进程崩溃,未刷盘的数据虽可通过 WAL 恢复,但仍有一定风险(如磁盘损坏)。生产环境需配合合理的备份与复制策略。
3. LSM-Tree:为什么要换数据结构
RocksDB 的核心是 LSM-Tree(Log-Structured Merge Tree)。理解它为什么快,先看看传统 B+ Tree 的问题。
3.1 B+ Tree 的麻烦
MySQL 的 InnoDB 用 B+ Tree 存数据。写入一条记录的典型流程(最坏情况):
- 找到目标 Page(16KB)
- 如果 Page 不在 buffer pool 中,从磁盘读取(随机 IO,约 10ms)
- 在内存中修改 Page
- 先写 redo log(WAL),保证持久性
- 返回成功;脏页后续异步刷回磁盘
问题很明显:每次写入都可能触发磁盘随机读。Page 满了还要分裂,产生更多 IO。高并发时锁竞争也激烈。
根本原因是 B+ Tree 的结构——数据必须按主键在 Page 内有序存储,所以每个写入都可能触发磁盘寻道和 Page 重排。
3.2 LSM-Tree 换个思路
LSM-Tree 完全换了一种思路:写要快到极致,读可以稍慢(但会用各种优化来加速读)。
写入流程就三步:
- 写 WAL 日志(顺序写磁盘)
- 写 MemTable(内存里的跳表)
- 返回成功
全程没有随机磁盘 IO,没有锁竞争,就是顺序写 + 内存操作。
如果进程崩了怎么办?WAL 还在,重启时从 WAL 重放未 Flush 的数据就行。
读取流程相对复杂一些(最坏情况):
- 查 MemTable(内存)
- 查 Immutable MemTable(内存)
- 查 Level 0 的 SST 文件(磁盘)
- Level 0 中不同文件的 key 范围可能重叠,因此需要检查所有 Level 0 文件
- 查 Level 1 ~ N 的 SST 文件(磁盘)
- Level 1 及以上同层内文件 key 范围不重叠且有序,可通过二分查找快速定位到唯一可能包含该 key 的文件
为了减少磁盘读取,RocksDB 给每个 SST 文件建了布隆过滤器,可以快速判断一个 key 是否一定不在某个文件里,从而跳过大量无效文件。
读性能确实比 B+ Tree 差一点,但换来的是极高的写入吞吐。实际生产环境中,通过合理配置 block cache、布隆过滤器和前缀迭代器,读性能可以满足绝大多数场景。
4. 数据是怎么组织的
LSM-Tree 的数据分两层:内存层和磁盘层。
内存层有两个部分:
- MemTable:活跃的内存结构,所有写入先到这里
- Immutable MemTable:MemTable 写满(默认 write_buffer_size = 64MB)后变只读,等待 Flush 到磁盘
磁盘层按 Level(层级) 组织,每层包含多个 SST 文件:
- Level 0:直接从 Immutable MemTable Flush 而来,文件之间 key 范围可能重叠
- Level 1 及以上:同一层级内各 SST 文件的 key 范围不重叠,且每个文件内部 key 有序
每层容量大概是上一层的 10 倍(可通过 max_bytes_for_level_base 调整)。读取时从低层往高层查,一旦找到就返回。
5. Compaction:写快背后的代价
LSM-Tree 写快,但有一个隐藏成本叫 "写放大"。
5.1 为什么要 Compaction?
Level 0 的文件会不断累积,文件越多读性能越差。所以需要 Compaction 把 Level 0 的文件合并到 Level 1,保持数据有序、减少文件数量。
触发条件:Level 0 文件数超过 level0_file_num_compaction_trigger(默认 4 个)。
过程大致如下:
- 从 Level 0 选出若干个 key 范围有重叠的文件
- 找出 Level 1 中与这些文件 key 范围重叠的所有文件
- 读取这些文件,合并排序
- 合并过程中删除被覆盖的旧 key(同一 key 在更高层级的是最新版本)
- 生成新文件写回 Level 1
- 删除旧文件
Compaction 不只是"合并",还负责去重和清理失效数据(如被删除的 tombstone)。
5.2 写放大从哪来?
Compaction 的代价是同一份数据被反复读写。举个例子:
- 你写入 1 MB 新数据到 Level 0
- Level 0 触发 Compaction,这 1 MB 会与 Level 1 中重叠范围的多个文件(总大小可能几十 MB)合并,然后写回 Level 1。实际写入磁盘的字节数远大于 1 MB
- Level 1 满了后,数据会继续被 Compaction 到 Level 2,再次产生写入
- 每下沉一层,数据都可能被重写一次甚至多次
写放大 = 实际写入磁盘的字节数 / 用户数据字节数
默认配置下,写放大通常在 10~30 倍之间。通过调整参数(如增大 max_bytes_for_level_base、启用 level_compaction_dynamic_level_bytes)可以显著降低。
写入 1 GB 用户数据,实际可能向磁盘写入 10~30 GB。
5.3 三个指标的权衡
LSM-Tree 有几个相互制约的指标:
- 写放大:实际写入磁盘 / 用户数据,典型值 10~30×
- 读放大:单次读需访问的文件数,典型值 1~10×
- 空间放大:实际占用 / 数据大小,典型值 1.1~2×
三者相互制约。RocksDB 提供了多种 Compaction 策略:
| 策略 | 适用场景 | 特点 |
|---|---|---|
| Level(默认) | 通用场景,读写较均衡 | 写放大可控,空间放大较小 |
| Universal | 写多读少,如日志数据 | 写放大更低,但读放大和空间放大稍高 |
| FIFO | 临时数据,如缓存 | 无真正的合并,旧文件直接删除,适用于 TTL 数据 |
5.4 常用调优参数(示例)
| 参数 | 作用 | 典型调整建议 |
|---|---|---|
| write_buffer_size | MemTable 大小 | 增大可减少 Flush 频率,但占用更多内存 |
| max_bytes_for_level_base | Level 1 总容量 | 增大可减少跨层 Compaction 次数,降低写放大 |
| level_compaction_dynamic_level_bytes | 动态调整层级大小 | 推荐开启,避免小数据量时空层级过多 |
| target_file_size_base | SST 文件目标大小 | 影响 Compaction 粒度 |
6. RocksDB 比 LevelDB 强在哪
RocksDB 是从 LevelDB fork 出来的,但做了大量工程优化:
多线程 Compaction LevelDB 只支持单线程 Compaction,RocksDB 支持多线程并行,速度大幅提升,写入停顿更少。
Column Families(列族) 可以在同一个 RocksDB 实例里创建多个独立的逻辑存储空间,每个 Column Family 有自己的 MemTable 和 SST 文件,共享 WAL。适合多租户场景。
前缀迭代器 针对范围扫描优化,如果 key 有共同前缀,可以利用布隆过滤器跳过不相关的 SST 文件。
DeleteRange 与 SingleDelete
- DeleteRange:删除一个连续的 key 范围,避免对该范围内每个 key 写入单独的 tombstone,效率极高
- SingleDelete:针对一个 key 只被写入一次然后被删除一次的场景(如临时状态)。如果误用(如覆盖写入后再 SingleDelete)会导致数据丢失,使用时需谨慎
动态层级大小 根据实际数据量自动调整各层级大小,避免小数据量时太多空层级。
MANIFEST 与 CURRENT RocksDB 通过 MANIFEST 文件记录所有 SST 文件的元数据(层级、key 范围、大小等),通过 CURRENT 文件指向当前最新的 MANIFEST。这使得数据库重启后能快速恢复状态。
7. 为什么分布式数据库都爱用 RocksDB
TiDB、CockroachDB 这些分布式数据库,存储层都用 RocksDB。
看 TiKV 的架构就明白了:
TiKV 节点:
┌─────────────────────────────────────────┐
│ gRPC API 层(处理 TiDB 的 KV 请求) │
├─────────────────────────────────────────┤
│ Raft 共识层(分布式一致性) │
├─────────────────────────────────────────┤
│ MVCC 事务层(版本控制) │
├─────────────────────────────────────────┤
│ RocksDB(单机 KV 存储) │
└─────────────────────────────────────────┘TiKV 自己实现了分布式层(Raft 共识、分片调度、分布式事务),但把单机数据持久化完全交给 RocksDB。这种分层设计让 TiKV 团队能专注于分布式问题,存储引擎的可靠性由 RocksDB 保证。
分布式数据库需要一个可靠的单机存储组件:高性能写入、持久化保证、嵌入式部署、活跃社区。RocksDB 正好满足这些条件。
8. 怎么选存储方案
| 对比项 | RocksDB | SQLite | Redis | MySQL |
|---|---|---|---|---|
| 数据模型 | KV | 关系型(SQL) | KV | 关系型(SQL) |
| 部署方式 | 嵌入式 | 嵌入式 | 独立服务 | 独立服务 |
| 持久化 | 是(WAL + SST) | 是 | 可选(RDB/AOF) | 是 |
| 读性能 | 中等(LSM-Tree) | 高(B+ Tree) | 极高(内存) | 高(B+ Tree + cache) |
| 写性能 | 极高 | 中等 | 极高(但可能丢数据) | 中等 |
| 典型场景 | 嵌入式存储底座、写密集 | 移动端、小规模应用 | 缓存、会话存储 | OLTP 业务 |
快速选型建议:
- 需要 SQL、JOIN、复杂查询 → SQLite 或 MySQL
- 纯 KV、高并发写入、需要持久化 → RocksDB
- 缓存、可丢数据、极低延迟 → Redis
- 完整的数据库服务、事务、二级索引 → MySQL(RocksDB 只是其一个可选存储引擎,非主流)
9. 最后说几句
RocksDB 适合的场景:
- 需要持久化,不能丢数据
- 写入密集,读相对较少
- 只需要 KV 接口
- 不想运维独立数据库
- 作为分布式系统的存储底座
不太适合的场景:
- 需要 SQL、JOIN、复杂查询
- 纯读多写少(LSM-Tree 读性能不如 B+ Tree)
- 磁盘空间紧张(写放大导致实际占用大于原始数据)
没有最好的存储引擎,只有最适合的。选型之前,先想清楚自己的读写比例和数据特性。
10. 参考文献与延伸阅读
- RocksDB Wiki – Compaction
- RocksDB Wiki – Write Amplification
- O'Neil, P., et al. (1996). The Log-Structured Merge-Tree (LSM-Tree). Acta Informatica.
- Facebook Engineering Blog: RocksDB: A Persistent Key-Value Store for Flash and RAM Storage (2013)