WAL必须原子写且调用fsync()强制落盘,否则日志可能丢失;需校验、序列号、完整上下文;恢复须幂等重放,严格遵循WAL与数据文件的两阶段提交顺序。

WAL 必须是原子写 + 持久化,fsync() 不可省略
很多实现者以为把日志追加到文件末尾就完成了 WAL,结果崩溃后发现最后几条记录丢失——根本原因是操作系统或磁盘缓存未落盘。write() 只保证数据进入内核页缓存,不保证写入物理介质。必须在每次提交日志后调用 fsync()(Linux/macOS)或 FlushFileBuffers()(Windows),否则 WAL 失去意义。
常见错误:ofstream —— flush() 仅清空 C++ 流缓冲区,不触发内核持久化;fflush() 同理,只作用于 libc 缓冲区。
- 打开日志文件时务必使用
O_WRONLY | O_APPEND | O_SYNC(Linux)或至少O_WRONLY | O_APPEND+ 手动fsync() - 避免批量合并多条日志再
fsync():单次fsync()延迟约 1–10ms,但能保证该次提交的原子性;异步批量会牺牲崩溃一致性 - 若用
mmap()写日志,必须配合msync(..., MS_SYNC),MS_ASYNC不满足 WAL 要求
日志格式要包含校验、序列号和完整操作上下文
只存 key 和 value 是不够的。崩溃恢复时需精确判断哪些日志已生效、哪些被截断、哪些损坏。典型结构应含:log_entry_header(含 magic number、length、crc32、seq_num、op_type)、payload(如 PUT/DEL 的完整 KV)、footer(可选 padding 或 trailer)。
容易踩的坑:seq_num 必须单调递增且连续(跳号即表示中间有丢失),不能用时间戳替代;crc32 要覆盖 header + payload,且计算前确保内存对齐、字节序一致(推荐 network byte order)。
-
op_type至少区分PUT、DEL、NOOP(用于填充或 checkpoint 标记) - 所有字符串字段(如
key)必须带长度前缀,禁止依赖 '\0' 截断——二进制 value 可能含 \0 - 避免在日志中存指针或相对偏移;恢复时地址空间已重置,必须全量序列化
崩溃恢复必须从头扫描 + 幂等重放,不能跳过“已完成”的日志
恢复逻辑不是“找到最后一个合法日志然后重放”,而是:打开 WAL 文件 → 逐块读取 header → 验证 crc → 解析 op → 对照当前 DB 状态判断是否需要执行(例如 DEL 一个已不存在的 key 应静默忽略)。关键在于所有操作必须幂等。
典型错误是试图靠 checksum 或文件 size 判断“有效日志边界”——文件可能因掉电被截断在 record 中间,header length 字段本身也可能损坏。正确做法是:每读一个 header,先检查 magic 和 length 是否合理(比如 length > sizeof(header) 且
- 恢复前先
fstat()获取文件大小,但不作为日志终点;按 record 边界逐步解析,遇到第一个非法 record 即停止 - 对每个
PUT,先查 DB 是否已有该 key 且版本号 ≥ 当前 log seq_num;若有且更新时间晚,则跳过(说明该日志已在上次正常 shutdown 前提交) - 必须在重放完成后再删除 WAL 文件,或重命名为
.wal.done;直接 unlink 会导致下次启动误判为无日志
WAL 和主数据文件的更新顺序必须严格遵循两阶段提交语义
KV 数据库的真正难点不在日志写入,而在如何让 WAL 和实际数据页更新形成原子性联动。标准做法是:1)写 WAL 并 fsync();2)修改内存中 data structure(如跳表节点、hash bucket);3)将脏页刷入主数据文件(如 mmap + msync() 或 pwrite() + fsync());4)仅当第 3 步成功后,才允许后续 WAL 记录覆盖旧日志(如循环 WAL)。
若跳过第 1 步或第 4 步,就会出现“日志有记录但数据没更新”或“数据已更新但日志被覆盖”的不一致。更隐蔽的问题是:主数据文件 fsync() 成功后,若进程在更新 WAL 元数据(如 offset 文件)前崩溃,重启时会重复重放——所以 WAL 元数据(如 current_offset)本身也要作为 WAL 的一部分持久化。
- 不要用单独的 offset 文件;把“下一个可用 log position”作为 WAL 文件末尾的固定 footer 字段,与最后一条 log 一起
fsync() - 主数据文件若用
mmap(),必须用MAP_SHARED且每次修改后调用msync(addr, len, MS_SYNC),MS_ASYNC不安全 - 禁用编译器/CPU 指令重排影响顺序:在
fsync()前插入std::atomic_thread_fence(std::memory_order_seq_cst)(虽通常非必需,但在某些弱序平台可防意外)
最易被忽略的是日志回滚边界:WAL 不是无限增长的,但截断(truncate)必须等对应日志所涉的所有数据变更都已安全落盘并 checkpoint 完成。哪怕只差一次 fsync(),提前清理 WAL 就等于主动放弃一致性。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











