本节摘要:切到 WAL 模式后,
xxx.db-wal是系统里最重要的新文件。它的结构出人意料地简单:一个 32 字节文件头加一串"帧",每帧 24 字节帧头加一整页数据。本节逐字段解析这个结构,讲清盐值与校验和分别防止哪类损坏,检查点的三种力度,以及 WAL 文件无限增长的四个常见病因。
一个刚提交过几个事务的 WAL 文件,前几十字节大致长这样(十六进制示意):
偏移 字节 含义 0-3 37 7f 06 82 或 37 7f 06 83 魔数,末位区分校验和的大端小端 4-7 00 2d e2 18 文件格式版本号 3007000 8-11 00 00 10 00 页大小 4096 12-15 00 00 00 03 检查点序号 16-23 12 ab 34 cd 56 78 9a bc 盐值 salt-1 加 salt-2 24-31 cc 12 34 56 78 9a bc de 首个校验和 32起 帧序列 ... 每帧 24 字节头 + 一整页
此后每一帧的结构完全一致:24 字节帧头加 4096 字节页数据。帧头四个字段:页号(这帧记录的是哪个页的新版本)、提交易(非零表示"本帧所在的提交已写库至此页数",零表示本帧之后还有同事务的帧)、两个盐值副本、两个校验和。整个文件就是"页的新版本流"——写入永远在文件尾部追加,永不修改已有帧。

文件结构只解决了"新页在哪",还差"读者怎么挑版本"。答案在伴生的 <库名>-shm 共享内存文件里:它保存 WAL 索引——页号到 WAL 帧位置的映射表,加若干"读标记"(read-mark)。读事务开始时在某个提交帧位置落一个标记,此后它读任何页先查索引:WAL 里该页有比标记新的帧就用它开始时的那个版本,没有就直读主文件。不同时刻开始的读事务落在不同标记上,各自看到稳定一致的世界——这就是 WAL 模式的快照隔离,4.4 节会把它放进三库隔离对照表里。
WAL 不能无限长,把帧内容回写主文件的动作用户。设页大小 4096,默认阈值下 WAL 长到约 4MB 就会触发自动检查点(wal_autocheckpoint 默认 1000 页)。三种力度:
PRAGMA wal_checkpoint(TRUNCATE) 常用于运维收尾。PRAGMA wal_checkpoint(PASSIVE) 的返回值是一行四列:忙标记、WAL 总页数、已检查点页数、检查点后可安全覆盖的页数——排查 WAL 增长问题时先看这行。
⚠️ WAL 无限增长的四个常见病因:一是有个长命读事务把检查点挡在原地(快速诊断:查
PRAGMA wal_checkpoint返回的忙标记);二是禁用了自动检查点或阈值设得过大;三是并发写太猛,追加速度长期高于 PASSIVE 检查点的推进速度;四是文件系统残留——进程异常退出后 WAL 未被清理,重新打开库会自动恢复,但前提是三件套(db、wal、shm)在同一目录里。
把三家的"新世界落盘"放在一起看:SQLite 的 WAL 帧是整页快照追加,同一页可多版本共存于文件中,检查点负责归并;InnoDB 的 redo 是物理修改记录追加,页本身不进日志,由后台线程按检查点刷脏页;PostgreSQL 的 WAL(同名不同物)也是修改记录流,但配合"堆页多版本"的设计,恢复时重放修改即可。SQLite 的整页方案牺牲了日志体积(一次提交哪怕只改一行也要落一整页),换来的是恢复逻辑极简与实现极小——嵌入式哲学在日志层的又一次体现。
三种力度的典型用法排成一张速查表:
| 场景 | 命令 | 期望结果 |
|---|---|---|
| 日常收尾(连接关闭前) | PRAGMA optimize; | 引擎按需做小检查点与统计更新 |
| WAL 偏大的主动清理 | PRAGMA wal_checkpoint(PASSIVE); | 能推多少推多少,绝不阻塞业务 |
| 备份或拷贝前置 | PRAGMA wal_checkpoint(TRUNCATE); | WAL 归零,之后拷主文件即安全 |
| 诊断卡死原因 | 观察 PASSIVE 返回的四列 | 忙列为 1 即有读者挡路 |
返回行的读法再强调一次:第二列是 WAL 里现有帧数,第三列是本次成功回写主文件的帧数,第四列是回写后可以安全覆盖的帧数。第三列远小于第二列即"有东西挡着"——通常是某个长命读事务,去应用侧找那个迟迟不结束的连接,比反复跑检查点有效。
**shm 文件可以删吗?**连接全部关闭后,wal 与 shm 会自动清理(或保留一个归零的空壳,取决于版本与关闭方式),此时删除无害。运行期间删 shm 等于抽掉读者的版本地图,轻则查询报错,重则索引错乱——把它当主文件的一部分对待,与 8.4 节的"三件套"纪律一致。
**WAL 模式能多人写吗?**能,但任意时刻仍只有一个写事务在写。多个连接可以轮流开写事务(配合 busy_timeout 排队),只是不能并行写。WAL 解放的是"读与写"的并行,不是"写与写"的并行——单写者模型没有变,变的是读者不再被写者阻塞。
**检查点会不会把性能拖垮?**PASSIVE 检查点每推进一步都会让出控制权,正常负载下感知不到。真正会拖垮性能的是"检查点债务":WAL 长期膨胀后,一次 FULL 或 TRUNCATE 检查点要回写的页数巨大,造成一次长 IO 尖峰。防法是让自动检查点正常工作、不让长读事务赖着不走——债务管理,与缓存预算同一思想。
**切回 DELETE 模式后,WAL 文件去哪了?**切换命令会先把现有 WAL 的内容完整检查点回主文件,再移除 WAL 与 shm,保证切换零数据丢失——这也是为什么切换必须在没有活动事务时执行。做这条操作时留意返回值与文件系统上伴生文件的消失,确认干净收场,再进行后续的备份或分发动作。