Skip to content

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 存数据。写入一条记录的典型流程(最坏情况):

  1. 找到目标 Page(16KB)
  2. 如果 Page 不在 buffer pool 中,从磁盘读取(随机 IO,约 10ms)
  3. 在内存中修改 Page
  4. 先写 redo log(WAL),保证持久性
  5. 返回成功;脏页后续异步刷回磁盘

问题很明显:每次写入都可能触发磁盘随机读。Page 满了还要分裂,产生更多 IO。高并发时锁竞争也激烈。

根本原因是 B+ Tree 的结构——数据必须按主键在 Page 内有序存储,所以每个写入都可能触发磁盘寻道和 Page 重排。

3.2 LSM-Tree 换个思路

LSM-Tree 完全换了一种思路:写要快到极致,读可以稍慢(但会用各种优化来加速读)。

写入流程就三步:

  1. 写 WAL 日志(顺序写磁盘)
  2. 写 MemTable(内存里的跳表)
  3. 返回成功

全程没有随机磁盘 IO,没有锁竞争,就是顺序写 + 内存操作。

如果进程崩了怎么办?WAL 还在,重启时从 WAL 重放未 Flush 的数据就行。

读取流程相对复杂一些(最坏情况):

  1. 查 MemTable(内存)
  2. 查 Immutable MemTable(内存)
  3. 查 Level 0 的 SST 文件(磁盘)
  4. Level 0 中不同文件的 key 范围可能重叠,因此需要检查所有 Level 0 文件
  5. 查 Level 1 ~ N 的 SST 文件(磁盘)
  6. 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 个)。

过程大致如下:

  1. 从 Level 0 选出若干个 key 范围有重叠的文件
  2. 找出 Level 1 中与这些文件 key 范围重叠的所有文件
  3. 读取这些文件,合并排序
  4. 合并过程中删除被覆盖的旧 key(同一 key 在更高层级的是最新版本)
  5. 生成新文件写回 Level 1
  6. 删除旧文件

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_sizeMemTable 大小增大可减少 Flush 频率,但占用更多内存
max_bytes_for_level_baseLevel 1 总容量增大可减少跨层 Compaction 次数,降低写放大
level_compaction_dynamic_level_bytes动态调整层级大小推荐开启,避免小数据量时空层级过多
target_file_size_baseSST 文件目标大小影响 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. 怎么选存储方案

对比项RocksDBSQLiteRedisMySQL
数据模型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)