今日主题#
- 主主题:
整体架构与 LSM-Tree - 副主题:
核心对象关系预览
学习目标#
- 建立 RocksDB 的第一张全局图:数据从哪里写入、暂存、落盘、再被读取。
- 在本地源码里先认清几个最重要的对象:
DB / CF / MemTable / WAL / SuperVersion / VersionSet / MANIFEST。 - 理解 RocksDB 的 LSM 不只是
MemTable -> SST,而是一整套前台写入与后台整理协作系统。 - 给后续 Day 002 的打开流程、Day 003 的写路径留出明确阅读入口。
前置回顾#
- 这是 RocksDB 主线学习的正式 Day 001。
- 仓库里已有 Rocksdbskiplist.md,它更适合作为后续
MemTable / SkipList的补充,不算主线 Day。 - 今天的目标不是吃透每个细节,而是先建立“之后继续读源码时不容易迷路”的第一张地图。
先把 7 个概念说清楚#
如果 Day 001 一上来就直接看 DBImpl、ColumnFamilyData、SuperVersion,很容易把几个不同层次的概念混在一起。
先给一个最简版本:
DB- 整个 RocksDB 数据库实例
- 对外 API 的入口
- 里面可以有多个
CF
CF- Column Family,逻辑隔离的 key-space
- 可以把它近似理解成“同一个 DB 里的不同逻辑表”
MemTable- 某个
CF当前的内存写缓冲 - 不是整个 DB 只有一个,而是每个 CF 一套
- 某个
WAL- 整个
DB共用的预写日志 - 不是每个
CF一份
- 整个
SuperVersion- 某个
CF当前稳定可读视图 - 里面打包了这个
CF的mem + imm + current version
- 某个
VersionSet- 整个
DB的版本元数据管理器 - 它管理所有
CF的版本演进,而不是只管一个CF
- 整个
MANIFEST- 整个
DB的版本变更日志文件 VersionSet的变化最终会写到这里
- 整个
一句话记忆:
DB管全局CF管逻辑隔离MemTable和SuperVersion是 CF 维度WAL、VersionSet、MANIFEST是 DB 维度
先看关系图:
flowchart TD
DB["DB / DBImpl
整个数据库实例"] --> CFA["CF A"]
DB --> CFB["CF B"]
DB --> WAL["WAL
整个 DB 共用"]
DB --> VS["VersionSet
整个 DB 共用"]
VS --> MANI["MANIFEST
整个 DB 共用"]
CFA --> MEMA["mutable memtable"]
CFA --> IMMA["immutable memtable"]
CFA --> SVA["SuperVersion"]
CFA --> VERA["current Version"]
CFB --> MEMB["mutable memtable"]
CFB --> IMMB["immutable memtable"]
CFB --> SVB["SuperVersion"]
CFB --> VERB["current Version"]
SVA --> MEMA
SVA --> IMMA
SVA --> VERA
SVB --> MEMB
SVB --> IMMB
SVB --> VERB这张图里最容易看错的两点是:
SuperVersion不是 DB 级,而是 每个 CF 一份VersionSet不是 CF 级,而是 整个 DB 共用一份
用更直白的话再说一遍#
DB#
可以把 DB 理解成整个数据库实例本身。
它负责:
- 对外提供
Put / Get / Delete / Iterator - 持有所有 Column Family
- 持有 WAL、后台线程、VersionSet 这些全局资源
CF#
CF 是同一个 DB 里的逻辑隔离单元。
它负责:
- 自己的 key 空间
- 自己的内存态
- 自己的磁盘版本视图
它不负责:
- 独立 WAL
- 独立 MANIFEST
所以 CF 不是“一个独立小数据库”,而是“共享全局资源的一块逻辑分区”。
MemTable#
MemTable 是某个 CF 当前的内存写缓冲。
这里要特别记住:
- 每个 CF 都有自己的 memtable
- 而不是整个 DB 只有一个 memtable
更准确一点:
- 每个 CF 通常有:
- 1 个当前可写的
mutable memtable - 0 到多个等待 flush 的
immutable memtable
- 1 个当前可写的
WAL#
WAL 是整个 DB 共用的日志体系。
它负责:
- 在写入 memtable 之前,先把更新顺序化地记到日志里
- 崩溃恢复时,重新把丢失的内存状态打回 memtable
这里也要注意一个边界:
- 可以说“一个 DB 共用一套 WAL”
- 但不能太粗糙地说“整个 DB 永远只有一个 WAL 文件”
因为实际运行时通常是:
- 同一时刻有一个活跃 WAL 在追加写
- 同时还可能有若干旧 WAL 文件没删,等 flush 后再清理
SuperVersion#
SuperVersion 是某个 CF 给读路径提供的稳定视图。
你可以把它理解成:
- “这个 CF 现在读的时候该看哪几个地方”的打包结果
里面核心就是三部分:
memimmcurrent version
所以它的职责不是“管理版本历史”,而是:
- 让前台读线程拿到一个稳定、可引用、不会半路变化的读视图
VersionSet#
VersionSet 是整个 DB 的版本元数据管理器。
它负责:
- 管理所有 CF 的版本演进
- 管理文件号、MANIFEST、WAL 保留边界这些全局元数据
最容易混淆的是:
- 单个 CF 有自己的
Version - 整个 DB 才有一个
VersionSet
所以 VersionSet 更像“总账本管理器”,不是“某个 CF 当前文件列表本身”。
MANIFEST#
MANIFEST 是整个 DB 的版本变更日志文件。
它负责记录:
- 新增了哪些 SST
- 删除了哪些文件
- 哪个 CF 的版本发生了变化
- WAL 保留边界推进到了哪里
所以它不是数据文件本身,而是:
- “数据库版本怎么变化过”的持久化日志
先建立 LSM 的直觉图#
如果只想快速抓住 RocksDB 的第一层直觉,下面这张图是很好的入口:
- 外部参考图:
这张图很适合用来记住最粗的一条主线:
- 写入先进入
WAL - 同时进入
MemTable MemTable满了之后变成immutable memtable- 再 flush 成
SST - 最后通过 compaction 逐层整理
但它没有展开的部分也要提前知道:
- 多个
CF怎么共存 SuperVersion为什么是读路径关键对象VersionSet / MANIFEST怎么管理版本元数据
所以 Day 001 要做的,不只是“记住 LSM 箭头”,还要把这些对象的层次分清。
源码入口#
D:\program\rocksdb\include\rocksdb\db.hD:\program\rocksdb\db\db_impl\db_impl.hD:\program\rocksdb\db\column_family.hD:\program\rocksdb\db\db_impl\db_impl.ccD:\program\rocksdb\db\db_impl\db_impl_write.ccD:\program\rocksdb\db\db_impl\db_impl_compaction_flush.ccD:\program\rocksdb\db\version_set.hD:\program\rocksdb\db\dbformat.h
它解决什么问题#
RocksDB 不走“直接原地改写磁盘页”的路线,而是把写入拆成几步:
- 先顺序写 WAL,保证崩溃后能恢复。
- 先写入内存中的
MemTable,把前台随机写改成内存操作。 - 再由后台把内存整理成 SST,并持续做 compaction。
但只知道 MemTable -> SST -> Compaction 还不够。真正的工程难点是:
- 读请求要同时看哪些地方。
- 后台 flush / compaction 改变状态时,前台读如何拿到稳定视图。
- 多个 Column Family 共存时,谁负责把“某个列族当前的内存态和磁盘态”收在一起。
它是怎么工作的#
先看整体数据流:
flowchart LR
A["客户端 Put/Delete"] --> B["DBImpl::WriteImpl"]
B --> C["WriteToWAL"]
B --> D["MemTable"]
D --> E["Immutable MemTable"]
E --> F["Flush"]
F --> G["L0 SST"]
G --> H["Compaction"]
H --> I["L1~Ln SST"]
J["客户端 Get/Iterator"] --> K["DBImpl::GetImpl"]
K --> L["获取 SuperVersion"]
L --> M["mutable MemTable"]
L --> N["immutable MemTable(s)"]
L --> O["current Version 对应的 SST"]再看对象关系:
flowchart TD
DBI["DBImpl"] --> CFD["ColumnFamilyData"]
DBI --> VSET["VersionSet"]
CFD --> MEM["MemTable"]
CFD --> IMMLIST["MemTableList"]
CFD --> CURVER["current Version"]
CFD --> SV["SuperVersion"]
SV --> MEM
SV --> IMMLIST
SV --> CURVER
VSET --> CFD今天要先记住五个结论:
DBImpl是总调度器。ColumnFamilyData是“单个列族的运行时状态容器”。MemTable是每个 CF 自己的内存写缓冲,而不是全库唯一。SuperVersion是每个 CF 的稳定读视图。VersionSet负责全库的版本元数据与 MANIFEST 相关演进。
关键数据结构与实现点#
DBImpl#
它不是单纯的 API 实现类,而是 RocksDB 的主引擎。
ColumnFamilyData#
它不是一个轻量 handle,而是一个列族当前运行态的聚合点。
SuperVersion#
它不是额外包装层,而是“让读线程拿到稳定快照视图”的关键对象。
VersionSet#
它不直接服务前台读,而是维护磁盘版本视图、文件元数据和版本切换。
源码细读#
下面不再整段搬源码,而是挑最值得带着问题回看的 5 个片段。
1. 从 DB 落到 DBImpl#
如果你之后自己读源码,第一步先建立这个入口感:外部拿到的是 DB 抽象,真正工作的是 DBImpl。
关键片段:
// db/db_impl/db_impl.h, class DBImpl
class DBImpl : public DB {
public:
DBImpl(const DBOptions& options, const std::string& dbname,
const bool seq_per_batch = false, const bool batch_per_txn = true,
bool read_only = false);这一段说明了什么:
DBImpl直接继承DB。- 之后看到
DB::Open、Put、Get之类对外 API,往下追时第一反应就应该是去找DBImpl对应实现。
这段在主流程中的位置:
- 它不是业务逻辑,而是“源码导航入口”。
读者后续自己读源码时先看哪里:
- 先从
include/rocksdb/db.h看公开 API。 - 再跳到
db/db_impl/db_impl.h和相应db_impl_*.cc找实现。
2. ColumnFamilyData 为什么是核心运行时容器#
如果只把列族理解成 key 空间分组,会低估 RocksDB 的对象边界。
关键片段:
// db/column_family.h, class ColumnFamilyData
MemTableList* imm() { return &imm_; }
MemTable* mem() { return mem_; }
bool IsEmpty() {
return mem()->GetFirstSequenceNumber() == 0 && imm()->NumNotFlushed() == 0;
}
Version* dummy_versions() { return dummy_versions_; }
Version* current() { return current_; } // 要求:持有 DB mutex
这一段说明了什么:
- 同一个
ColumnFamilyData里直接挂着:- 当前可写
mem - 不可变
imm - 当前磁盘版本
current
- 当前可写
- 也就是说,列族不是“名字”,而是“一整组 LSM 运行时状态”。
为什么这里重要:
- 以后无论看 flush、compaction、open、read path,很多逻辑最后都会落到“拿某个
cfd的mem/imm/current”。
本节先不展开什么:
- 这里先不细讲
TableCache、MutableCFOptions等更外围字段,Day 001 只抓最关键的三件套。
3. SuperVersion 为什么值得单独记#
真正的读路径不是直接盯着一堆全局可变状态读,而是拿一个稳定视图。
关键片段:
// db/column_family.h, class ColumnFamilyData
SuperVersion* GetSuperVersion() { return super_version_; }
SuperVersion* GetReferencedSuperVersion(DBImpl* db);
SuperVersion* GetThreadLocalSuperVersion(DBImpl* db);
bool ReturnThreadLocalSuperVersion(SuperVersion* sv);
void InstallSuperVersion(SuperVersionContext* sv_context,
InstrumentedMutex* db_mutex,
std::optional<std::shared_ptr<SeqnoToTimeMapping>>
new_seqno_to_time_mapping = {});这一段说明了什么:
SuperVersion不是临时局部技巧,而是列族上的一等对象。- 它有获取、引用、归还、安装这一整套生命周期接口。
- 这意味着读路径、后台线程、TLS 缓存、旧视图回收都围绕它组织。
为什么这里重要:
- 读懂 RocksDB 的前提之一,就是接受“读线程拿到的不是最新可变状态本体,而是一个稳定视图对象”。
读者后续自己读源码时先看哪里:
- Day 001 先记接口。
- Day 002 再回看
InstallSuperVersion(...)和打开流程。 - 之后到 read path 再细看
GetThreadLocalSuperVersion(...)。
4. 写路径第一层骨架:WAL、memtable、状态切换#
今天不细拆并发写线程和 sequence 分配,但至少要知道写路径最后把状态推向哪里。
关键片段:
// db/db_impl/db_impl_write.cc, memtable 切换与 InstallSuperVersionAndScheduleWork(...) 调用点
cfd->mem()->SetNextLogNumber(cur_wal_number_);
assert(new_mem != nullptr);
cfd->imm()->Add(cfd->mem(), &context->memtables_to_free_);
if (new_imm) {
new_imm->SetNextLogNumber(cur_wal_number_);
cfd->imm()->Add(new_imm, &context->memtables_to_free_);
}
new_mem->Ref();
cfd->SetMemtable(new_mem);
InstallSuperVersionAndScheduleWork(cfd, &context->superversion_context);这一段说明了什么:
- 当前 memtable 会被转入
imm()。 - 新 memtable 会成为新的活跃 memtable。
- 状态切换之后会立刻安装新的
SuperVersion。
为什么这里重要:
- 这段把“内存态切换”与“读视图发布”直接连了起来。
- 它说明 RocksDB 的状态推进不是改几个指针就完,而是要同步更新读路径可见视图。
本节先不展开什么:
WriteImpl(...)前面大量参数检查、写线程分组、WAL 写入细节先不展开。- 这些放到 Day 003 单独讲更合适。
5. 读路径为什么先拿 SuperVersion 再定默认 snapshot#
这是 Day 001 最值得提前记住的一段,因为它能避免之后理解 snapshot 时走偏。
关键片段:
// db/db_impl/db_impl.cc, DBImpl::GetImpl(...)
SuperVersion* sv = GetAndRefSuperVersion(cfd);
SequenceNumber snapshot;
if (read_options.snapshot != nullptr) {
...
} else {
// 注意:必须先引用 super version,再设置 snapshot;
// 否则如果中间发生 flush,可能会把该 snapshot 需要的数据 compact 掉。
snapshot = GetLastPublishedSequence();
...
}这一段说明了什么:
- 默认 snapshot 不是随便什么时候拿都行。
- 读路径先固定住
SuperVersion,再拿GetLastPublishedSequence()。
为什么这里重要:
- 如果先拿 sequence、后拿视图,中间发生 flush/compaction,读者可能看到一个前后不一致的世界。
- 所以 RocksDB 很强调“可见性”和“视图”的绑定顺序。
读者后续自己读源码时先看哪里:
- 之后到
Snapshot / Sequence Number / 可见性语义那天,再回到这段注释。 - 那时这段会从“先记住”升级成“彻底闭环”。
再补一小段,把“拿到 SuperVersion 之后怎么归还”补完整。
关键片段:
// db/db_impl/db_impl.cc, DBImpl::GetAndRefSuperVersion(...) / DBImpl::ReturnAndCleanupSuperVersion(...)
SuperVersion* DBImpl::GetAndRefSuperVersion(ColumnFamilyData* cfd) {
return cfd->GetThreadLocalSuperVersion(this);
}
void DBImpl::ReturnAndCleanupSuperVersion(ColumnFamilyData* cfd,
SuperVersion* sv) {
if (!cfd->ReturnThreadLocalSuperVersion(sv)) {
CleanupSuperVersion(sv);
}
}这一段说明了什么:
- 读路径拿的并不一定是“现拿现建”的
SuperVersion,而是优先走线程本地缓存。 - 归还时也不是一律直接删除,而是先尝试放回线程本地;只有放不回去时才真正做 cleanup。
为什么这里重要:
- 它把 Day 001 里“读路径依赖稳定视图”再往前推进了一步,变成“读路径依赖的是可复用、可延迟清理的稳定视图对象”。
- 这也是为什么后面读
GetImpl()、MultiGet()、iterator 相关代码时,会反复看到“先取 sv,最后归还 sv”这条固定节奏。
读者后续自己读源码时先看哪里:
- 先回看
DBImpl::GetImpl()里GetAndRefSuperVersion(...)和ReturnAndCleanupSuperVersion(...)的成对出现。 - 再看
ColumnFamilyData::GetThreadLocalSuperVersion(...)和ReturnThreadLocalSuperVersion(...),确认SuperVersion的线程本地复用是怎么落地的。
6. 后台调度不是附属,而是 LSM 主体的一部分#
最后看一眼后台调度骨架,就知道 compaction/flush 不是可有可无的收尾,而是系统核心。
关键片段:
// db/db_impl/db_impl_compaction_flush.cc, DBImpl::MaybeScheduleFlushOrCompaction(...)
void DBImpl::MaybeScheduleFlushOrCompaction() {
mutex_.AssertHeld();
if (!opened_successfully_) {
return;
}
if (bg_work_paused_ > 0) {
return;
}
...
while (!is_flush_pool_empty && unscheduled_flushes_ > 0 &&
bg_flush_scheduled_ < bg_job_limits.max_flushes) {
env_->Schedule(&DBImpl::BGWorkFlush, fta, Env::Priority::HIGH, this,
&DBImpl::UnscheduleFlushCallback);
--unscheduled_flushes_;
}
...
}这一段说明了什么:
- RocksDB 会持续根据当前状态安排后台 flush / compaction。
- 前台写入只是把数据推进系统,后台任务负责把系统维持成可控的 LSM 结构。
为什么这里重要:
- Day 001 就要先建立一个判断:RocksDB 的核心不是某个单独数据结构,而是“前台快写 + 后台整形”的协作系统。
今日问题与讨论#
我的问题#
问题 1:为什么 GetImpl() 要先拿 SuperVersion,再决定默认 snapshot?#
- 简答:
- 因为读请求必须先固定“要读的那一份 mem/imm/version 视图”,否则 flush/compaction 会让 sequence 和视图错位。
- 源码依据:
D:\program\rocksdb\db\db_impl\db_impl.cc
- 当前结论:
SuperVersion和 snapshot sequence 在读路径上必须按顺序绑定。
- 是否需要后续回看:
是
问题 2:为什么 ColumnFamilyData 同时持有 mem、imm、current version?#
- 简答:
- 因为它们共同组成了“这个列族当前的 LSM 运行态”,拆散之后读、写、flush、compaction 的协作边界会很乱。
- 源码依据:
D:\program\rocksdb\db\column_family.h
- 当前结论:
ColumnFamilyData是列族级运行时容器,不是简单 handle。
- 是否需要后续回看:
是
问题 3:为什么状态切换后要立刻安装新的 SuperVersion?#
- 简答:
- 因为旧的读视图还可能被引用,而新的读请求又必须看到新的 mem/imm/current 组合,系统需要明确的“视图发布”动作。
- 源码依据:
D:\program\rocksdb\db\db_impl\db_impl_write.ccD:\program\rocksdb\db\column_family.h
- 当前结论:
- RocksDB 把“切状态”和“发布读视图”视为一组操作。
- 是否需要后续回看:
是
外部高价值问题#
- 今日未引入外部问题。
- 原因:
- Day 001 先建立本地源码驱动的第一张全局图,避免一开始就被外部讨论分散注意力。
常见误区或易混点#
- 误区 1:LSM 只等于“一堆 SST 文件”
- 更准确地说,LSM 是
WAL + MemTable + immutable memtable + SST levels + compaction的整体协作系统。
- 更准确地说,LSM 是
- 误区 2:
VersionSet就是前台读请求直接使用的当前视图- 前台读路径真正拿的是
SuperVersion。
- 前台读路径真正拿的是
- 误区 3:列族只是 key 的逻辑分组
- 从源码看,每个列族几乎都有自己的一整组 memtable / version / flush / compaction 状态。
- 误区 4:compaction 只是“清理重复 key”
- 它还在维持层级组织、读放大、写放大、空间放大的整体平衡。
设计动机#
为什么 RocksDB 更偏爱 LSM 而不是原地更新#
如果采用原地更新,随机写会更频繁地打到磁盘页,I/O 模式和写放大都不够友好。LSM 的取舍是:
- 前台写变快:先 WAL + 内存
- 后台整理变复杂:需要 flush 和 compaction
- 读路径变复杂:需要同时看内存态和磁盘态
RocksDB 明显选择了“把复杂性转移到后台和元数据管理”,换前台写吞吐和更友好的写模式。
为什么要有 SuperVersion#
如果每次读都盯着一组全局可变状态看,flush、compaction、iterator、snapshot 很容易相互打架。SuperVersion 的价值是:
- 给读者一个稳定视图对象
- 避免读路径长期持锁
- 让旧资源的生命周期跟着视图引用自然回收
横向对比#
| 维度 | RocksDB / LSM | 传统 B+Tree 式存储 |
|---|---|---|
| 前台写入 | 先 WAL,再内存,尽量避免随机磁盘写 | 更偏向原地更新或页分裂 |
| 后台维护 | 依赖 flush / compaction 持续整理 | 更依赖页管理与 buffer pool |
| 读路径 | 可能同时看 memtable、immutable、多个 level | 通常围绕树页遍历 |
| 可见性语义 | 经常和 sequence / snapshot 绑定 | 常结合事务 / MVCC |
工程启发#
- 高并发读系统里,给读者一个“稳定视图对象”,往往比让所有人直接看全局可变状态更可控。
- 把“前台必须快”的路径和“后台可以慢慢做”的路径拆开,是 RocksDB 很典型的工程取舍。
- 学 RocksDB 时,先抓对象边界比先钻函数细节更重要,否则很容易在代码里迷路。
今日小结#
今天最重要的收获不是记住了多少函数,而是建立了一张后续读源码不容易迷路的地图:
- 对外入口是
DB/DBImpl - 列族运行时核心是
ColumnFamilyData - 读路径依赖
SuperVersion - 内存态由
MemTable + MemTableList组成 - 磁盘版本元数据由
VersionSet / Version维护
之后继续读源码时,可以按这条路径走:
- 先问自己当前看的是前台写、前台读、还是后台整理。
- 再找它落到哪个
cfd。 - 再看它动的是
mem、imm、current还是SuperVersion。
明日衔接#
下一天建议进入:DB 打开流程与核心对象关系
重点继续看:
D:\program\rocksdb\db\db_impl\db_impl_open.ccD:\program\rocksdb\db\version_set.ccD:\program\rocksdb\db\column_family.cc
要带着这三个问题去读:
DB::Open()如何恢复 MANIFEST / WAL?ColumnFamilyData、VersionSet、SuperVersion在打开阶段是怎么真正连起来的?- 为什么打开完成后,数据库就已经具备了一个可服务的稳定视图?
复习题#
- 为什么说 RocksDB 的 LSM 不只是
SST,而是一整套前台写入与后台整理协作系统? DBImpl、ColumnFamilyData、SuperVersion三者分别负责什么?- 为什么读路径要先拿
SuperVersion,再决定默认 snapshot sequence? MemTable和MemTableList在架构上分别代表什么状态?VersionSet和SuperVersion的职责边界有什么不同?


