WAL(预写日志)是保障数据可靠性与性能平衡的必要机制:必须先将操作追加写入带校验和、长度前缀和序列号的二进制日志并fsync成功,再更新内存;崩溃时通过重放完整日志恢复状态,避免直接依赖std::map+fstream导致的数据丢失。

为什么不能直接用 std::map + fstream 就完事?
因为宕机或程序崩溃时,std::map 在内存里的修改还没刷到磁盘,数据就丢了;而单纯每次写都 fsync() 又太慢。真实场景里得在「可靠性」和「性能」之间找平衡点——比如 WAL(Write-Ahead Logging)是绕不开的起点。
你不需要从零造 LSM 或 B+Tree,但得明确:简单 ≠ 随意序列化。下面几步是底线:
- 所有写操作先追加写入一个
wal.log文件(带校验和、长度前缀、递增序列号) - 成功
fsync()后,再更新内存std::unordered_map<:string std::string></:string> - 读操作只查内存哈希表;启动时重放
wal.log恢复状态 - 定期把内存快照 dump 成
snapshot.dat,并截断旧 WAL
WAL 文件格式怎么设计才不容易出错?
别用 JSON 或文本行——解析慢、容错差。用二进制固定头 + 可变体最稳妥。一个典型 record 结构:
4 字节 len | 1 字节 op_type (0=SET, 1=DEL) | 4 字节 key_len | key_bytes | 4 字节 val_len | val_bytes
关键细节:
立即学习“C++免费学习笔记(深入)”;
-
len是整个 record 的字节数(不含自身),用于跳过损坏 record -
op_type必须有,否则删键后重放会漏操作 - 每次写完 record 立即
write()+fsync(),不要缓冲 - 打开 WAL 时用
ios::ate | ios::binary,读取前先seekg(0, ios::beg)
如何安全地做 snapshot + WAL 截断?
不能边 dump 边接受写入,否则 snapshot 和 WAL 状态不一致。必须用双缓冲 + 原子切换:
- 写新 snapshot 到临时文件
snapshot.tmp,dump 完毕后fsync() - 重命名
snapshot.tmp → snapshot.dat(Linux/macOS 上rename()是原子的) - 记录当前 WAL 最后一条 log 的序列号
last_seq到 snapshot 头部 - 截断 WAL:打开
wal.log,ftruncate()到last_seq对应位置之后
注意:ftruncate() 前必须确保所有写线程已停,否则可能截掉正在写的 record。
多线程访问时哪些地方必须加锁?
不是所有操作都要锁全局。按粒度拆:
- WAL 写入:必须独占锁(
std::mutex wal_mutex),因为write()+fsync()要串行 - 内存 map 读:用
std::shared_mutex,读多写少场景下能显著提升并发读性能 - snapshot 触发:由后台线程定时检查 size/时间阈值,触发时先 acquire
wal_mutex,再 acquiremap_mutex(写优先) - 禁止在锁内做
fsync()或长耗时 IO,容易卡住其他线程
真正难处理的是 crash 恢复逻辑——WAL 可能写到一半就断电,所以重放时要跳过不完整 record,靠 len 校验和双重判断。这点很容易被忽略,但决定了引擎是否真的“简单可靠”。


















