应用执行下面这行代码:
db.put(key, value);
方法返回成功时,数据在哪里?
它已经写进 SST 了吗?如果还在内存里,为什么进程崩溃后有机会恢复?WAL 和 MemTable 究竟是谁先写谁?当 MemTable 满了,前台写入是不是必须停下来等文件生成完?
这些问题听起来像是在问一个时间点,实际上混合了至少四个时间点:
- Put 刚进入 RocksDB;
- 当前写处理已经完成;
- MemTable 已经切换为 Immutable;
- Flush 生成的 L0 SST 已经发布。
如果不把它们拆开,“Put 成功”“当前可读”“进程崩溃可恢复”和“已经进入 SST”就很容易被误认为同一件事。
先给结论: 一次 Put 会进入 RocksDB 的统一写入处理。默认路径下,更新会记录到 WAL,并应用到 Mutable MemTable;WAL 为尚未 Flush 的更新提供恢复依据,MemTable 让最新状态参与当前读取。Put 返回并不意味着数据已经进入 SST。MemTable 切换后仍然可读,后台 Flush 构建并安全发布 L0 SST,内存与旧 WAL 的职责才逐步结束。
本文按逻辑阶段讲解,不把图画成固定的 C++ 调用栈。RocksDB 支持并发 MemTable Write、Pipelined Write 等路径,版本和选项也会改变内部并行方式。我们关心的是稳定的职责边界,而不是把某个版本的一组函数名背下来。
示例使用 org.rocksdb:rocksdbjni:10.10.1.1,概念边界对照 RocksDB 11.1.2 的公开资料。若你使用其他版本,应再核对对应 Javadoc、Release Notes 与配置默认值。
先看全景:Put 返回只是路径中的一个边界
先把整条 Write Path 放在一张图里。
Put 返回、MemTable 切换和 Flush 完成是三个不同时间点。
这张图可以分成两部分。
前台写处理负责让本次更新进入数据库当前状态:
RocksJava Put
→ 统一写入入口
→ WAL 恢复记录(默认路径)
→ Mutable MemTable 当前状态
→ 根据写入选项完成返回
图里把 WAL 和 MemTable 并列,是为了强调它们是同一次写处理中的两个职责,而不是“二选一”的两条产品方案。精确的追加、同步和 MemTable Apply 可以因写入模式并发或流水化,本篇不承诺一个适用于所有配置的函数先后表。
后台生命周期负责把内存状态交接给不可变文件:
Mutable MemTable
→ Immutable MemTable
→ 后台 Flush
→ 构建 SST
→ 发布到 L0
所以,当别人问“Put 成功以后数据在哪”,最稳妥的第一句不是“在 WAL”或“在 SST”,而是反问:你关心的是当前读取、崩溃恢复,还是文件持久化?
从 RocksJava 的一次 Put 开始
先看一个尽量完整的最小例子:
import java.nio.charset.StandardCharsets;
import org.rocksdb.Options;
import org.rocksdb.RocksDB;
import org.rocksdb.RocksDBException;
import org.rocksdb.WriteOptions;
public final class WritePathDemo {
public static void main(String[] args) throws RocksDBException {
RocksDB.loadLibrary();
byte[] key = "user:42".getBytes(StandardCharsets.UTF_8);
byte[] value = "Alice".getBytes(StandardCharsets.UTF_8);
try (Options options = new Options().setCreateIfMissing(true);
WriteOptions writeOptions = new WriteOptions()
.setDisableWAL(false)
.setSync(false);
RocksDB db = RocksDB.open(options, "./data/write-path-demo")) {
db.put(writeOptions, key, value);
}
}
}
对应的 Maven 依赖可以写成:
<dependency>
<groupId>org.rocksdb</groupId>
<artifactId>rocksdbjni</artifactId>
<version>10.10.1.1</version>
</dependency>
这里有三个容易被“示例太短”掩盖的工程边界。
第一,RocksDB.loadLibrary() 会加载 native 库。RocksJava 不是纯 Java 实现,真正的数据结构、文件和后台线程位于 native RocksDB 中。
第二,Options、WriteOptions 和 RocksDB 都持有 native 资源,需要及时关闭。try-with-resources 会按照声明的逆序先关闭数据库,再关闭写选项和数据库选项。生产代码还要确保数据库不会在业务请求仍使用它时被提前关闭。
第三,Key 与 Value 都是字节数组。UTF-8 只是这个示例的编码约定,不是 RocksDB 自动理解的业务类型。
代码中的三个对象分别控制什么?
Options 描述数据库级与默认 Column Family 相关的打开配置,WriteOptions 描述这一次写调用采用的写入边界,RocksDB 则代表已经打开的数据库实例。把它们拆开很重要,因为“数据库怎样组织文件”和“某次写入是否等待 WAL 同步”不是同一层决策。
示例显式写出 disableWAL=false 与 sync=false,不是说生产环境应该照抄,而是为了让两个开关同时出现在视野里。真正选择时要先回答:这份数据是否可以从上游重放?应用承诺的 RPO 是多少?同步等待增加的尾延迟是否可接受?同一路径中的不同业务写入能否采用不同可靠性等级?
还要注意,成功创建 Java 对象不代表数据目录已经具备正确的运维条件。生产环境至少要确认目录所在文件系统、磁盘空间、权限、备份恢复方案和单进程打开边界。RocksDB 是嵌入式库,不会替应用启动一个独立服务来管理这些生命周期。
如果 db.put 抛出 RocksDBException,调用方不能假设写入成功;如果方法正常返回,也只能按照当前 WriteOptions 与已验证的故障模型解释成功边界。API 返回值本身不会替团队定义业务级“绝不丢失”。
Java 调用经过 JNI 进入 native RocksDB 后,单条 Put 不会绕开数据库的统一写入秩序。它同样要与其他线程、日志、Sequence Number 和 MemTable 协调。
单条 Put 也会进入统一写入入口
从 API 看,put 只包含一条 Key-Value;从写入内核看,它仍是一项需要排队和排序的写操作。
如果同一时刻有三个线程:
Writer A: Put user:42 = Alice
Writer B: Put order:9 = PAID
Writer C: Delete cart:3
RocksDB 需要建立统一的写入顺序,不能让三个线程各自随意追加日志、分配版本并修改共享 MemTable。
为减少每个小请求都单独承担一次固定日志或同步开销,RocksDB 会尝试把兼容的 Writer 组成一个 Write Group。组内请求可以共享底层 WAL 处理,再把各自操作应用到 MemTable。
Group Write 摊薄固定开销,但不能假设所有并发请求必然进入同一批。
这张图故意使用“可能”。是否能进入同一个 Group,会受到 WriteOptions、请求类型、队列时机和内部模式影响。并发抵达也不意味着所有请求必然共享一次同步边界。
还要区分两个经常混在一起的概念:
- Write Group 是 RocksDB 内部为一轮处理聚合兼容 Writer 的机制;
- WriteBatch 是调用方显式构造的 API 对象,可以把多条 Put/Delete 作为一个批次提交。
它们都叫“Batch”或“Group”,却不处在同一个抽象层。WriteBatch 的原子性、大小边界和并发语义会在进阶篇单独讨论。
为什么统一入口不能省?
假如每个线程都独立决定自己的 Sequence、独立向同一 WAL 任意写入,再同时修改共享 MemTable,恢复时就很难重建一个一致的新旧顺序。统一入口的价值不只是“排队”,而是把日志记录、顺序分配、内存应用和错误结果放进一个可协调的写入协议。
这也解释了为什么高并发不一定线性提高吞吐。更多 Writer 可以增加组成 Group 的机会,却也可能增加排队、锁竞争、内存分配和设备同步压力。当底层 WAL 或存储设备已经饱和,继续增加业务线程往往只是把等待从 RocksDB 外移到队列里。
观察写入性能时,应把“调用前在业务线程池等待”“进入 RocksDB 后排队”“等待同步”“应用到 MemTable”和“被后台 Stall”区分开。一个端到端 P99 只能告诉你慢了,不能单独证明慢在 WAL、MemTable 还是 Compaction。
写入顺序由谁标记?
假设先后发生:
Put user:42 = Alice
Put order:9 = PAID
Put user:42 = Bob
Alice 与 Bob 的 User Key 相同。即使它们后来分散在不同 MemTable 或 SST,读取也必须知道 Bob 更新。
RocksDB 使用单调递增的 Sequence Number 建立全局写入顺序。可以先把每条操作想象成带着这样的标签:
Seq 101 user:42 = Alice
Seq 102 order:9 = PAID
Seq 103 user:42 = Bob
Sequence Number 先解决“谁更新”,完整可见性规则留给进阶篇。
Sequence Number 不只是一个展示给用户看的时间戳。它会进入 RocksDB 的内部排序与版本可见性判断。同一个 WriteBatch 内多条操作如何分配顺序、Snapshot 为什么只能看到某个 Sequence 上界、Internal Key 怎样编码 User Key、Sequence 和 Value Type,这些都留到 A01。
B03 只保留一个最小模型:
- 更大的 Sequence 通常代表更新的写入;
- 当前读视图有一个可见上界;
- 同一个 User Key 可能保留多个 Sequence;
- “记录更新”和“记录对某个 Snapshot 可见”不是同一句话。
WAL 与 MemTable:同一次写入的两种职责
有了顺序以后,更新需要同时解决两个问题。
第一个问题是:当前 Get 怎样立刻读到 Alice?
如果 Put 必须等待 SST 文件生成才能可读,前台延迟就会被每次 Flush 的文件构建工作绑住。因此,最新记录先进入 MemTable,读取也会优先检查内存中的新状态。
第二个问题是:Alice 还没有进入 SST,进程崩溃后怎样恢复?
内存结构会随进程消失,所以默认路径还要把更新记录到 WAL。数据库重新打开时,可以重放仍然有效的 WAL,重建尚未被 SST 覆盖的内存状态。
WAL 回答怎样恢复,MemTable 回答现在怎样读写。
因此,下面三句话分别是错的:
- “写了 WAL,所以不需要 MemTable”;
- “写进 MemTable,所以已经进入 SST”;
- “sync=false,所以根本没有 WAL”。
默认 disableWAL=false 时,普通写入会记录 WAL。sync 是另一个维度:
| WriteOptions | 主要变化 | 不能直接推导出的结论 |
|---|---|---|
disableWAL=false |
更新参与 WAL 路径 | 不等于任何断电场景都零丢失 |
disableWAL=true |
跳过 WAL | 尚未 Flush 的数据不再拥有同样恢复边界 |
sync=false |
不为本次写入等待同样的同步动作 | 不等于没有写 WAL |
sync=true |
返回前请求并等待更强的 WAL 同步 | 不等于脱离文件系统和硬件就能承诺绝对持久 |
“崩溃”不是一种故障
讨论 WAL 时必须说明故障模型:
- Java 进程异常退出;
- 操作系统崩溃;
- 主机重启;
- 突然断电;
- 存储设备缓存没有掉电保护;
- 文件系统或介质损坏。
这些故障会跨越不同的缓存和持久化边界。WAL 为恢复提供结构化记录,但“记录已经交给哪个层”“哪个层在故障后仍可靠”还要结合同步调用、文件系统、挂载选项、设备缓存与硬件保障分析。
所以,基础篇只使用这个审慎结论:
在未禁用 WAL 的常见路径中,WAL 为尚未被 SST 覆盖的更新提供恢复依据;
sync=true请求更强的返回前同步边界,精确 RPO 必须放回具体系统验证。
用三个崩溃时刻理解 WAL 的保护窗口
做一个思想实验。数据库最初没有 user:42,应用准备写入 Alice。
场景一:调用刚进入,写处理尚未完成时进程退出。
调用方没有拿到成功结果,新值也可能尚未形成可恢复记录。恢复后是否存在 Alice 不能靠业务意愿猜测,调用方应按失败或结果未知的接口约定处理;需要幂等时,应在 Key、请求标识或上层协议中设计去重边界。
场景二:默认 WAL 路径的 Put 已完成,但 MemTable 尚未 Flush。
旧进程的 MemTable 会消失。重新打开数据库时,RocksDB 读取已经存在的 SST,并重放仍有效的 WAL,把尚未获得 SST 覆盖的更新重新构造成内存状态。恢复完成后,普通 Get 才能再次读到 Alice。
这正是 WAL 最典型的保护窗口:更新已经进入当前数据库状态,但还没有完成从内存到不可变文件的职责交接。
场景三:L0 SST 已经安全发布以后进程退出。
Alice 已经由当前 Version 中的 SST 承担读取职责。相关 WAL 是否立刻可删仍取决于数据库整体状态,尤其是同一日志中是否包含其他 Column Family 尚未 Flush 的数据,但这条记录本身不再只依赖旧 MemTable 才能恢复。
再把场景二改成 disableWAL=true:如果 Alice 尚未 Flush,重启时就没有同等的 WAL 重放依据,它可能丢失。这不是 RocksDB 偷偷忽略了写入,而是调用方主动选择了不同的恢复边界。
思想实验没有解决突然断电时设备缓存是否持久的问题,却先把“进程内存消失”和“存储介质是否真正落稳”拆开了。前者说明为什么需要 WAL,后者说明为什么还要讨论 sync、fsync 和硬件。
Mutable MemTable 怎样接住当前写入?
Mutable MemTable 是当前可写的内存表。Put、Delete 和 Merge 等操作被应用后,会按 Internal Key 的顺序组织在其中。
RocksDB 默认常见的 MemTable Representation 是 Skip List,但也支持 HashSkipList、HashLinkList、Vector 等其他实现。不同实现会改变插入、点查、迭代和内存开销,不能把“MemTable 就是 Skip List”写成永远成立的定义。
对 Write Path 来说,Mutable MemTable 最重要的职责有三项:
- 接收新的记录;
- 让最新记录立即参与普通读路径;
- 为后续有序 Flush 提供可迭代内容。
如果 Alice 已在旧 SST,Bob 刚进入 Mutable MemTable,普通 Get 应先看到 Bob。旧 SST 并没有被原地修改,只是被更高 Sequence 的记录遮蔽。
MemTable 位于 native memory 侧,不能只看 JVM Heap 估算应用内存。Block Cache、Write Buffer、Index/Filter、JNI 对象和后台任务都会占用不同的内存区域。内存治理会在工程篇展开。
Mutable 什么时候变成 Immutable?
活跃 MemTable 达到切换条件后,RocksDB 会把它冻结成 Immutable MemTable。Immutable 的含义是“不再接收新写入”,不是“不能读取”。
与此同时,新的 Mutable MemTable 接管前台写入。
双缓冲让前台和 Flush 并行,但 Immutable 队列仍有容量边界。
假设旧表中有 Alice,切换后执行 Put Bob:
Immutable: user:42 = Alice
Mutable: user:42 = Bob
读取会先考虑新的 Mutable,再考虑 Immutable,所以仍返回 Bob。后台可以独立遍历旧表并生成 SST,新写入无需修改正在 Flush 的内容。
不要把 write_buffer_size 当成唯一触发器。MemTable 大小是常见条件,但手动 Flush、全局 Write Buffer Manager、Column Family 间的内存压力和其他内部机制也可能推动切换或 Flush。
这种前后台解耦有一个明确上限:如果 Flush 跟不上,Immutable MemTable 会越积越多。RocksDB 不可能无限占用内存,因此最终必须对前台施加背压。
Flush 怎样让一个 L0 SST 对读取可见?
后台 Flush 会按内部顺序遍历 Immutable MemTable,并使用当前 Table Format 构建 SST。
以常见 Block-Based Table 为直觉,SST 中会包含:
- 保存记录的 Data Block;
- 定位数据块的 Index;
- 可选的 Filter;
- 压缩、Checksum、Footer 与其他元数据。
文件写出后,还需要完成必要的校验与元数据发布,让新的 SST 进入数据库当前 Version。只有这次职责交接成功,读取才会把它作为 L0 候选。
写完文件与让文件对读取可见都完成后,旧内存和日志职责才逐步结束。
这里有三个不能压成一句话的步骤:
- 构建: 从 Immutable 的有序内容生成 SST 结构;
- 持久化与发布: 让文件及数据库元数据安全衔接;
- 回收旧职责: 释放不再需要的 MemTable,并在整体条件允许时处理旧 WAL。
WAL 与 SST 也不是一一对应。一个 WAL 可能包含多个 Column Family 的写入;只要某些相关内存状态尚未获得安全的 SST 覆盖,这段日志就可能仍有恢复职责。
因此,“一个 MemTable Flush 完,所以对应 WAL 立刻删除”不是可靠的通用结论。
自动 Flush 与手动 Flush 应该怎样理解?
正常运行时,RocksDB 会根据内存与写入状态安排后台 Flush。应用也可以显式调用 Flush API,但“能手动触发”不意味着每次 Put 后都应该调用。
如果每条写入后都强制 Flush,就会破坏 MemTable 聚合大量小更新的意义,制造更多小 SST,并把本可批量完成的文件构建工作推到前台节奏附近。是否需要手动 Flush,应该由备份、Checkpoint、关闭流程、测试控制或明确的运维目标驱动,而不是把它误当成 sync=true 的替代品。
两者解决的问题不同:WAL 同步关注尚未 Flush 写入的日志持久化边界;Flush 关注把一批内存状态转换成 SST。Flush 完成也不自动等于目录中所有文件都经过 Compaction,更不代表所有旧版本已经回收。
在线上观察时,还要区分“Flush 已调度”“正在构建文件”和“输出已经安装”。只有最后的元数据发布完成,新的 SST 才加入当前读取版本。一次耗时较长的 Flush 可能慢在排队、设备写入、压缩或元数据收尾,不能只看调用名称下结论。
用四个时刻回答“数据在哪里”
现在可以把开头的问题做成一张时间表。
问“数据在哪里”必须先说明是在 Put 的哪个时刻。
T1:Put 刚进入
请求可能仍在 JNI 调用、写队列或当前 Group 中。此时不能假设新值已经对其他读可见,也不能宣布它已经拥有完整的恢复边界。
T2:当前写处理完成
在默认 WAL 路径的简化模型中,更新已有 WAL 记录并应用到 Mutable MemTable。当前普通读可以从 MemTable 找到新值;如果数据还没进入 SST,恢复依赖有效 WAL。
Put 何时向调用方返回还受 sync、错误、写入模式等影响,所以图中使用“写处理完成”而不是给所有配置强行定义一条机器指令级返回线。
T3:MemTable 已成为 Immutable
旧表不再接收新写入,但仍然可读。新 Mutable 接管更新,后台 Flush 旧表。此时 Alice 可能在 Immutable,Bob 在新 Mutable,而对应 WAL 仍承担未完成交接部分的恢复职责。
T4:L0 SST 已发布
Flush 输出已经安全加入当前 Version,读取可以从 L0 找到相关记录。旧 Immutable 可以释放,相关 WAL 只有在数据库整体条件满足时才可能被回收。
同一条数据从内存到 SST 不是瞬间移动,而是一次带恢复保护的职责交接。
为什么 Put 有时会变慢甚至暂停?
前台写入不断把工作交给后台:MemTable 要 Flush,L0 SST 要继续 Compaction。如果生产速度长期高于消费能力,队列就会增长。
RocksDB 官方 Write Stalls 文档归纳了三类关键压力:
- 等待 Flush 的 MemTable 太多;
- L0 文件数量太多;
- Pending Compaction Bytes 太高。
Write Stall 是防止内存、读放大和空间失控的背压机制。
为什么不是让队列继续长?
因为每类积压都会伤害另一条路径:
- Immutable 过多会占用更多内存,并扩大需要恢复的在途状态;
- L0 文件过多会增加重叠候选与读放大;
- Pending Bytes 过高说明未来需要大量后台 I/O 和临时空间;
- 存储设备长期饱和会同时拉高前台读写尾延迟。
因此 RocksDB 会在某些条件下先降低写入速率,压力更大时暂时停止新写入,让 Flush 或 Compaction 追上。
Stall 不是参数越大越好解决。盲目提高阈值只会允许队列积得更深,可能把一次较早、较短的背压变成更晚、更长的延迟尖峰。正确诊断需要把应用写入速率、Immutable 数、L0 文件、Pending Bytes、后台线程、设备带宽和空间放在同一条时间线上。
具体阈值、指标与调优会放到工程篇;基础篇先记住因果链:
前台写得比后台处理快
→ MemTable / L0 / Pending Bytes 积压
→ 读取、内存与空间风险上升
→ Slowdown / Stop 保护系统
怎样从线上证据定位 Write Path 变慢?
当业务反馈“RocksDB 写入突然慢了”,最容易做错的是立刻改大所有阈值。更可靠的做法是先把一次写入拆成前台、日志、内存和后台四组证据。
第一步:确认慢的是哪里
先比较业务请求总耗时与 db.put 调用耗时。如果前者慢、后者稳定,问题可能在业务队列、序列化或锁;如果 Put 本身的 P99 上升,再继续看 RocksDB 与设备。
同时确认慢的是所有 Column Family,还是只有某一个写入热点。多个 Column Family 可以共享 WAL 与后台资源,但各自 MemTable、L0 和 Compaction 状态不同。只看数据库总吞吐,可能把一个局部积压误判成全局设备故障。
第二步:检查前台写入边界
确认 disableWAL、sync、WriteBatch 大小和值大小是否发生变化。一次同步小写、一次包含几千条记录的批量写、一个数 MB 的大 Value,虽然都叫 Put 或 Write,前台成本完全不同。
如果同步写比例突然提高,设备同步延迟可能直接进入请求尾延迟;如果 Batch 过大,单个 Writer 会更长时间占据一轮处理,也可能影响其他请求等待。此时只增加业务线程数通常无助于降低单次设备同步时间。
第三步:看 MemTable 与 Flush
观察活跃和 Immutable MemTable 的数量、内存占用、Flush 速率与 Flush 耗时。如果 Immutable 持续增长,说明新表生成得比旧表落盘更快。要继续确认是后台线程不足、压缩耗 CPU、设备写入慢,还是其他 Column Family 抢占了 Flush 资源。
单次出现一个 Immutable 很正常;关键是它能否在负载稳定时回落。诊断应看时间序列,而不是看到某个非零数值就报警。
第四步:看 L0 与 Compaction 债务
如果 MemTable 能顺利 Flush,但 L0 文件数与 Pending Compaction Bytes 持续上涨,瓶颈已经从“内存到 L0”移动到“L0 向后续层级整理”。此时 Put 变慢可能是 Compaction Stall,而不是 WAL 或 MemTable 插入变慢。
还要把设备读写带宽、I/O 利用率、队列深度和尾延迟放在一起。Compaction 同时读旧 SST、写新 SST;设备显示高吞吐不一定代表健康,它也可能已经饱和,导致前台 WAL 与后台文件 I/O 互相干扰。
第五步:做收支判断,而不是只看阈值
最终要回答两个速率:前台每秒制造多少待 Flush、待 Compaction 数据,后台每秒能稳定消化多少。如果前者长期更大,扩大缓冲区只会延迟 Stall 到来的时间;只有减少输入、增加有效后台能力、改善文件组织或更换资源边界,才能让系统回到稳定状态。
一份有用的故障记录至少应保留负载变化前后几分钟的应用延迟、写入字节、MemTable 状态、L0 文件、Pending Bytes、Flush/Compaction 吞吐、磁盘空间和设备延迟。把这些曲线对齐,通常比复制一份“最佳 RocksDB 参数”更快接近根因。
六个常见误区
误区一:Put 返回时数据一定已经在 SST
数据通常先由 MemTable 提供当前可读状态。SST 要等 MemTable 切换并由后台 Flush 构建、发布。
误区二:WAL 写完以后,RocksDB 才开始碰 MemTable
WAL 和 MemTable 是同一次写处理中的恢复与查询职责。并发写、Pipelined Write 等模式会改变内部重叠方式,不应把基础图当成所有版本的固定函数调用栈。
误区三:sync=false 就没有 WAL
是否写 WAL 由 disableWAL 控制;sync 关注是否为该写入等待更强同步动作。这是两个不同开关。
误区四:启用 WAL 就能保证所有断电场景零丢失
进程、内核、文件系统、设备缓存与介质是不同层级。可靠性承诺必须明确同步配置、故障模型和硬件条件。
误区五:MemTable 只由 write_buffer_size 触发切换
它是重要条件之一,但全局内存预算、手动 Flush 和其他机制也会影响切换与 Flush。
误区六:Write Stall 是 RocksDB 随机卡住
Stall 通常是可观察的后台积压结果,是保护内存、读取和空间边界的背压。真正的问题是为什么后台长期跟不上。
再走一遍 Put user:42
把所有机制压回一次写入:
- RocksJava 把
Put("user:42", "Alice")通过 JNI 送入 native RocksDB; - 写操作进入统一队列,可能与兼容 Writer 组成 Group;
- RocksDB 为写入建立 Sequence Number 顺序;
- 默认路径记录 WAL,并把记录应用到 Mutable MemTable;
- 根据 WriteOptions 和处理结果,Put 完成并返回;
- Mutable 达到切换条件后成为仍可读的 Immutable;
- 新 Mutable 接管后续写入,后台 Flush 旧表;
- Flush 构建 SST 并通过元数据发布到 L0;
- 旧 MemTable 释放,相关 WAL 在整体条件满足时才可能回收;
- 若 Flush 与 Compaction 长期慢于写入,背压会让 Put 减速或暂停。
一句话总结:
WAL 保护尚未完成文件交接的更新,MemTable 服务当前状态,Flush 把内存职责安全交给 L0 SST;Put 返回只是这条生命周期上的前台边界。
下一篇 B04《图解 RocksDB Read Path:一次 Get 如何找到最新值》,我们会从写入留下的多个位置出发,回答 Get("user:42") 怎样先看 Mutable 与 Immutable,再用文件范围、Bloom Filter、Index Block 和 Block Cache 缩小 SST 候选。
