上一章结尾欠的账现在还:事务凭什么叫「提交了就永不丢」(持久性 D)?凭什么「说回滚就一丝不留」(原子性 A)?答案全在一本流水账里。想象一种极端场景:数据库正在跑十几个事务,机房突然断电,内存(缓冲池里几十万条脏页)瞬间蒸发,只剩磁盘上参差不齐的现场——有的页是新的,有的是旧的,有的写了半页。重启之后,数据库要回答两个问题:哪些改动要补写回去?哪些改动要抹掉? 回答这两个问题的依据只有一个:磁盘上的日志。本章讲这本账怎么记、什么时候写、崩了之后怎么按账重建。
| 故障 | 典型场景 | 丢了什么 | 急救手段 |
|---|---|---|---|
| 事务故障 | 除零、死锁被选为牺牲者 | 单个事务的中间状态 | undo 回滚 |
| 系统故障 | 断电、内核 panic | 内存全部,磁盘完好 | redo + undo |
| 介质故障 | 磁盘坏、误删数据文件 | 磁盘数据本身 | 备份 + 日志重放 |
分档的意义:前两档磁盘数据没坏,只需要把「内存里没来得及落盘的已提交修改」补上(redo),把「没提交但已污染磁盘的修改」抹掉(undo)。第三档最狠,数据文件本身没了,救回来的原材料只剩「上一个备份 + 备份之后的全部日志」,备份间隔直接决定你最多丢多少数据(灾备术语叫 RPO)。顺带一提,「删库跑路」在介质故障那一档,运维圈那句「备份不做,十年白忙」就是给它准备的。
日志里记两类条目,目的相反:
<T, A, 新值>。事务 T 把 A 改成了什么。用途:崩溃后把已提交事务的改动重放出来,撑持久性。<T, A, 旧值>。事务 T 动 A 之前的原值。用途:把未提交事务的改动撤销回去,撑原子性。一笔转账(A 减 100,B 加 100)在日志里的样子:
<T, 开始>
<T, A, 旧=1000, 新=900>
<T, B, 旧=500, 新=600>
<T, 提交>
恢复时读账本判生死:见 <T, 提交>,T 的所有改动重做(哪怕数据页已经落盘,重放一遍幂等无害);没见提交,T 的所有改动逆序撤销。一套账本,两个方向,解决全部前两档故障。
恢复逻辑成立有一个前提:崩溃后日志必须比数据新。如果数据页先落盘、日志还在路上就断电,已提交的改动将无据可查。所以恢复体系的基石是那条著名的规则:
WAL(Write-Ahead Logging,预写式日志):任何数据页落盘之前,它对应的日志必须先落盘。
这条规则的妙处在于它同时是个性能优化。没有 WAL 的世界,每个提交都要把脏页刷盘,而脏页散在磁盘各处,全是随机写;有了 WAL,提交只需要把日志顺序追加写盘(顺序写比随机写快百倍,第 9 章的磁盘物理),脏页攒着慢慢刷。第 6 章讲过 SSD 怕随机小写,第 9 章会算机械盘顺序写的账,WAL 把「可靠性」和「性能」拧成了一个方向,这是存储领域最优雅的设计之一。Redis 的 AOF、Raft 的复制日志、Kafka 的分区日志,全是 WAL 思想的变体:先把意图顺序写下来,再慢慢消化。
日志越积越长,如果恢复要从开天辟地扫起,重启一次要几小时,等于换个姿势宕机。**检查点(checkpoint)**定期「结一次账」:把当前脏页全部刷盘、在日志里写下 <CHECKPOINT, 活跃事务清单>。之后的恢复从最近检查点起算,之前的账目「已两讫」不用再翻。
…… [检查点 C2] ……………… [崩溃]
恢复动作:C2 之后——已提交的 redo,未提交的 undo
检查点频率是道权衡题:密则恢复快(账本短)、但平时刷盘开销大;疏则平时快、重启久。InnoDB 的模糊检查点、PG 的 bgwriter 都在自动调这个平衡,运维只需看指标(innodb_checkpoint_age)别顶到上限。
把三档故障的处置串成剧本:
事务故障(活着的时候就处理):事务报错或死锁,用它的 undo 记录把改动逐个逆序抹掉,别的事务毫无感知。
系统故障(重启时处理):扫描最近检查点之后的日志;redo 已提交事务(按日志顺序重放新值);undo 未提交事务(逆序回旧值)。两阶段做完,磁盘回到「所有已提交事务生效、未提交痕迹归零」的一致点。
介质故障(换上备份后处理):装入最近一次全量备份(或全量 + 增量),再从备份时点起重放归档日志,追到崩溃瞬间。这套「备份 + 日志链」是所有数据库灾备的原型,MySQL 的 binlog 主从复制其实是「边重放边给另一台机器做」的实时版。
innodb_flush_log_at_trx_commit = 1 + sync_binlog = 1(双 1)是资金类业务的标配,关掉换 TPS,掉电窗口变大,改之前算清 RPO。<T1 开始> <T1 A 旧9 新10> <T2 开始> <T2 B 旧7 新8> <T1 提交> [崩溃],写出恢复动作(A、B 各自最终值,谁被 redo 谁被 undo)。<T1 提交> 丢失(磁盘坏道),A 的最终值是多少?PRAGMA journal_mode;(默认 delete 模式),切到 WAL 后看看目录里多出的 -wal 文件;插一万行对比两种模式的写入耗时,亲测 WAL 的顺序写红利。