2026-10-26·作者 Sijie Wang#fundamentals#storage#wal#durability

wal

WAL:日志先行,所以提交可以又便宜又丢不了

它是干嘛的。 改动在内存里做,快;数据页落盘慢,还是随机写(一会儿这页一会儿那页)。但事务一提交就必须丢不了——断电、进程崩溃都不行。这个承诺是 Durability(持久性),ACID 的 D(Atomicity 原子性、Consistency 一致性、Isolation 隔离性、Durability 持久性;形容词 atomic、consistent、isolated、durable)。WAL(Write-Ahead Log,预写日志)就是不用每次提交都刷数据页、也能兑现这个承诺的办法。

铁律一条。 数据页可以晚落盘,但描述改动的日志记录,必须先落盘,这个事务才许提交。"先"(ahead)永远是相对于数据页落盘而言,不是相对于内存改动。

第一幕——顺利路径。 走一次提交(主角仍是 accounts id=7,balance 100 改 80,事务 742)。(1) 每次改动,先往 WAL 缓冲区追加一条日志记录——"事务 742 把某页改成了这样",每条带递增序号 LSN;然后才改内存里的数据页(mvcc 的版本照常生成)。(2) 敲下 COMMIT:把 WAL 缓冲刷盘 + fsync,fsync 落地之后才返回"提交成功"。注意此刻改过的数据页可能还只活在内存里——但这次提交已经完全算数,因为日志能把页重建出来。(3) 之后 checkpoint 在后台把脏页慢慢刷盘,并把"日志可以回收到哪一段"的界线往前推。顺利路径上不会发生的事:提交时刷数据页、提交时等 checkpoint。提交延迟约等于一次顺序追加 + 一次 fsync。(回滚也是常态流量:事务撤销自己的改动,撤销动作本身也记成日志——补偿记录——这样 undo 做到一半崩溃也不会把故事讲乱。)

第二幕——崩溃。 断电那一刻,内存全灭:没刷盘的数据页、WAL 缓冲里没 fsync 的尾巴,一起没。盘上幸存:已 fsync 的 WAL(到某个 LSN 为止)+ 以前 checkpoint 刷好的数据页(可能相当旧)。重启恢复三步,顺序不能乱。(1) 定位:从控制文件读出最后一个 checkpoint 的位置——只有它之后的日志可能还没进数据页。(2) Redo(先重做):按 LSN 顺序逐条重放,不管事务提交没提交,先把物理世界恢复成崩溃前最后一刻的样子。重放为什么放不坏:每个数据页页头存着 page LSN——"我已经吸收到的最新日志号";一条记录只有在目标页的 page LSN 落后于它时才真重放,否则跳过。于是重放幂等,恢复中途二次断电、重启再来一遍,结果不变。(3) Undo(再回滚):清点崩溃时未提交的事务,用日志把它们的改动逐条抹掉,抹的动作同样记成补偿记录。结束状态:已提交的一个不少,未提交的一个不剩——像崩溃只是一次暂停。

三份"副本"的图景。 同一个逻辑状态的三种时态:内存里的页是现在时,磁盘上的数据页是过去时,WAL 是把过去时推回现在时的那串动词。写成等式:数据页 +(checkpoint 之后那段)日志 ≡ 内存里的状态。三者在内容上并不相同,是靠那条铁律保持"代表同一状态"的等价。

为什么写日志便宜——它明明也在磁盘上。 三条。(1) 顺序 vs 随机:日志是往一个文件尾巴追加,笔不抬头;数据页散在整个文件各处,刷页是随机 IO——机械盘贵在寻道,SSD 贵在写放大。(2) 写得少:一条日志只记改动本身,几十到几百字节;刷一页是整页 8KB/16KB,改一个字节也全额陪绑。一个事务动了十个分散的页:刷页 = 10 次随机整页写,写日志 = 一小段连续追加。(3) 只写一处、还能拼单:一次提交只 fsync 一条流,排队的多个事务还能共享同一次 fsync(group commit)。WAL 不是不落盘,是把躲不掉的落盘整形成磁盘最喜欢的样子:顺序的、增量的、一次的。(合并整理的活由 checkpoint 在后台干:一页被改十次,只刷最终版一次。)

同族不同物——WAL 与 MVCC 版本。 两者都是只追加的改动记录,像不是错觉(InnoDB 的 undo log 还一物两用兼任旧版本仓库)。区别在:岗位不同(版本管活着时读写分离,WAL 管死后复活);形态不同(版本散在各行身上、没有全局顺序,WAL 是一条全局有序的流);最硬的一条是落盘纪律不同——版本可以只活在内存里、崩溃即蒸发,WAL 必须被逼着先落盘才算数。持久性来自这个"逼",不来自"叠"。

read next
關於這一條

sijie 的一條 wiki 條目。本站的 AI 以同一套語料庫為依據、以 sijie 的口吻作答,並附有指向此類條目的引用——回答會花 sijie 的錢,所以它在存取碼後等待: 輸入存取碼 →

wal — Sijie Wang