RocksDB源码-0x05-WAL

1 wal机制的作用

wal机制的作用是防crash,在crash发生后可以进行恢复。这个机制几乎在各个数据库都能见到。

2 wal文件目录和文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  rocksdb_ctest_put tree
.
├── CURRENT
├── IDENTITY
├── LOCK
├── LOG
├── MANIFEST-000005
├── OPTIONS-000007
├── sst
│   ├── flash_path
│   │   └── 000009.sst
│   └── hard_drive
└── wal
└── 000008.log

wal目录下放的是当前正在使用或者刚切换下来的wal

指定了wal目录就用指定的,没有指定就用db顶层目录放wal日志文件

wal下archive目录里面放着的是已经不再写但是暂时还不能删除的wal

3 wal文件格式

rocksdb_ldb dump_wal --walfile=wal/000004.log --header --print_value命令dump文件

1
2
3
4
5
6
7
8
9
10
11
12
13
➜  rocksdb_ctest_wal rocksdb_ldb dump_wal --walfile=wal/000004.log --header --print_value
Sequence,Count,ByteSize,Physical Offset,Key(s) : value
1, 1, 27, 0, PUT(0) : 0x68656C6C6F30 : 0x776F726C6430
2, 1, 27, 34, PUT(0) : 0x68656C6C6F31 : 0x776F726C6431
3, 1, 27, 68, PUT(0) : 0x68656C6C6F32 : 0x776F726C6432
4, 1, 27, 102, PUT(0) : 0x68656C6C6F33 : 0x776F726C6433
5, 1, 27, 136, PUT(0) : 0x68656C6C6F34 : 0x776F726C6434
6, 1, 27, 170, PUT(0) : 0x68656C6C6F35 : 0x776F726C6435
7, 1, 27, 204, PUT(0) : 0x68656C6C6F36 : 0x776F726C6436
8, 1, 27, 238, PUT(0) : 0x68656C6C6F37 : 0x776F726C6437
9, 1, 27, 272, PUT(0) : 0x68656C6C6F38 : 0x776F726C6438
10, 1, 27, 306, PUT(0) : 0x68656C6C6F39 : 0x776F726C6439
(Column family id: [0] contained in WAL are not opened in DB. Applied default hex formatting for user key. Specify --db=<db_path> to open DB for better user key formatting if it contains timestamp.)

这几列内容分别表示

  • 第1列 1 Sequence Number: 这是该记录的全局序列号。RocksDB里的每一条数据修改都有一个唯一的递增的序列号。它是实现快照读Snapshot Read和数据版本控制MVCC的核心。
  • 第2列 1 Count: 这个记录里包含的操作数量。因为是一次Put一个Key,所以这里是1。如果用了WriteBatch批量写入,这里会显示批次内操作的总数。
  • 第3列 27 Type: RocksDB内部的操作类型枚举值。27对应kTypeValue是普通的Put操作。
  • 第4列 0 Offset: 该记录在WAL文件中的字节偏移量。第一条在0,第二条在34,说明每条记录含头部占用了34字节。
  • 第5列 PUT(0): PUT是操作动作,括号里的0表示Column Family ID。没有创建多列族,数据默认都写在ID为0的default列族里。
  • 第6列 key的16进制
  • 第7列 value的16进制

4 wal的过程

4.1 wal回放前置准备

4.1.1 准备全量CF的VersionEdit容器

1
2
3
4
5
6
for (auto cfd : *versions_->GetColumnFamilySet()) {
// 给每个CF都准备一个VersionEdit壳子 如果在wal过程中发生了sst变更就更新这个VersionEdit 最终wal结束 这个VersionEdit就是wal过程中的增量
VersionEdit edit;
edit.SetColumnFamily(cfd->GetID());
version_edits->insert({cfd->GetID(), edit});
}

4.1.2 wal日志最小的文件编号要求

4.1.2.1 系统级的wal要求下限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 这是系统级的wal要求下限 保证memory table恢复一致+事务完整
* wal的编号要求分系统级要求和数据安全级要求两个
* 1 系统级要求比数据安全级的高 也就是系统级的wal编号小于数据安全级别的wal编号
* 2 数据安全级的wal编号只能用来做memory table恢复一致性
* 3 系统级别的wal编号还可以用不保证事务语义保证
*
* 1 如果wal日志删除早了 会导致数据永久丢失
* 2 如果wal日志删除晚了 会导致数据冗余在磁盘上浪费磁盘空间
* 本质是支撑着未flush数据的最早wal
* 只要一个wal对应的数据已经flush成sst并且被Version管理了那么这个wal就不用再参与到恢复
* @return 系统crash后 为了恢复到一致状态+事务状态完整 必须保留的最早的wal日志
*/
uint64_t DBImpl::MinLogNumberToKeep() {
return versions_->min_log_number_to_keep();
}
4.1.2.2 数据安全级的wal要求下限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* wal恢复memory table的要求下限 这是数据安全级别的要求 能保证的是恢复memory table的一致性 这个要求是宽泛的
* 如果是事务的两阶段提交 那么就要更严格的系统级的要求下限 保证memory table恢复一致+事务完整
*
* 为什么这个地方要讨论两阶段提交
* wal的唯一用途就是恢复memory table 也就意味着一旦memory table里面的数据flush到了sst文件 wal文件的使命就完成了可以删除了
* 两阶段提交的时候
* 1 prepare阶段写入了wal
* 2 此时还没完成commit所以数据对外是不可见的
* 3 commit可能在未来的wal中
* 所以如果只从memory table有没有flush到sst来判定wal可不可以删除 会导致prepare了还没commit的数据丢失 导致事务语义被破坏
* 所以在两阶段提交模式下 wal的用途不单单是保证memory table能恢复一致 还用于事务状态机的恢复
* 所以在两阶段下wal日志要更严格
*/
uint64_t MinLogNumberWithUnflushedData() const {
return PreComputeMinLogNumberWithUnflushedData(nullptr);
}

4.2 处理一个wal record

4.2.1 拿到WriteBatch逻辑协议

一旦从wal文件里面读到内容,就涉及到两层协议的解析

1
2
3
4
// 拿到WriteBatch协议 放在batch_to_use里面
process_status = InitializeWriteBatchForLogRecord(
record, reader, running_ts_sz, &batch, new_batch, batch_to_use,
record_checksum);

4.2.2 跳过协议头

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 1个WriteBatch里面可能会有多个put record
* 解析里面每个put record
*/
Status WriteBatch::Iterate(Handler* handler) const {
if (rep_.size() < WriteBatchInternal::kHeader) {
return Status::Corruption("malformed WriteBatch (too small)");
}
// WriteBatch逻辑协议带头 所以要跳过头部 直接跳到put record部分
return WriteBatchInternal::Iterate(this, handler, WriteBatchInternal::kHeader,
rep_.size());
}

4.2.3 put record的批量处理

1
2
3
// 在WriteBatch协议体里面处理每一个put操作的record 因为可能有多个put record 所以需要套个while
// todo 在处理一批put record过程中可能出现失败 已经处理过的就不管了 那么怎么保证原子性的 用MVCC机制实现版本可见性保证原子性
while (((s.ok() && !input.empty()) || UNLIKELY(s.IsTryAgain()))) {

怎么保证原子性和可见性的请见RocksDB源码-0x11-MVCC

4.2.4 怎么处理每个put record的

4.2.4.1 TLV解码
RocksDB源码-0x0D-协议设计TLV

所有的逻辑都在这个函数里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* input里面放了1个或多个put record
* 单独的put record的流怎么处理的
* 协议格式是tag+cf id+key的len+key+value的len+value
* 这个函数拿到流的第一个byte解析出来put的类型tag 用默认cf的tag有固定值 识别出来决定在协议里面有没有cf的id编码
* 推进协议流拿到所有的出参
* 这个函数只负责从协议里面解析内容 不做处理
* @param input 入参 WriteBatch逻辑协议体 已经跳过了逻辑协议头 指向的就是1个或多个put record 除了这个其他的都是出参
* @param tag put record的类型
* @param column_family 出参 cf的id 只有当协议不是默认cf的时候才会解析id 默认cf的id就是0
* @param key 出参 键
* @param value 出参 值
*/
Status ReadRecordFromWriteBatch(Slice* input, char* tag,
uint32_t* column_family, Slice* key,
Slice* value, Slice* blob, Slice* xid,
uint64_t* write_unix_time)

以其中一段为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 拿到协议的首字节 解码出tag 解码后推进丢掉这个字节
*tag = (*input)[0];
input->remove_prefix(1);
// 为了压缩设计 要是默认cf 在协议里面是抠掉cf id编码的 默认给cf是0 根据解码出来的tag看看协议里面有没有cf编码
*column_family = 0; // default
switch (*tag) {
case kTypeColumnFamilyValue:
if (!GetVarint32(input, column_family)) {
return Status::Corruption("bad WriteBatch Put");
}
FALLTHROUGH_INTENDED;
// 很巧妙的设计 给默认cf压缩编码 比如
// 如果不是默认cf的put操作 那么就会命中第一个case分支先解码出cf的id 因为没有break会继续执行第二个case分支的逻辑一直到break为止 所有会解码出key和value
// 如果是默认cf的put操作 那么就会命中第二个case分支 只会解码出key和value 调用方只要给cf_id=0表示默认cf就行了
case kTypeValue:
if (!GetLengthPrefixedSlice(input, key) ||
!GetLengthPrefixedSlice(input, value)) {
return Status::Corruption("bad WriteBatch Put");
}
break;
4.2.4.2 任务派发

怎么往memory table里面写数据会根据不同的put record类型进行分发

1
2
3
4
5
6
7
8
9
10
11
12
// 2 上面已经解析出来了put record 派发业务处理 真正的处理逻辑在ProtectionInfoUpdater里面
switch (tag) {
case kTypeColumnFamilyValue:
case kTypeValue:
assert(wb->content_flags_.load(std::memory_order_relaxed) &
(ContentFlags::DEFERRED | ContentFlags::HAS_PUT));
s = handler->PutCF(column_family, key, value);
if (LIKELY(s.ok())) {
empty_batch = false;
found++;
}
break;

真正的处理逻辑实现在RocksDB源码-0x12-WAL的PutRecord到MemoryTable

4.2.5 看看有没有刷盘任务要处理

1
2
3
4
// 已经把1个WAL文件回放到了内存 这批数据可能会导致内存被占满而触发产生了一个刷盘任务 因此这个时机看一下有没有刷盘任务要执行
process_status = MaybeWriteLevel0TableForRecovery(
has_valid_writes, read_only, wal_number, job_id, next_sequence,
version_edits, flushed);

关于内存刷盘持久化看RocksDB源码-0x13-内存数据刷盘SST0

至此,在启动时候从WAL恢复内存的流程就结束了。


RocksDB源码-0x05-WAL
https://bannirui.github.io/2026/01/30/RocksDB/RocksDB源码-0x05-WAL/
作者
dingrui
发布于
2026年1月30日
许可协议