数据库突然断电,凭什么没丢数据?------WAL 日志与崩溃恢复
数据库 · 事务与可靠性。这篇文章讲 redo/undo 日志、WAL(Write-Ahead Logging)协议,以及崩溃后数据库如何靠日志"恢复如初"------这是理解数据库可靠性的底层。
一次"拔电源"的粗暴测试
有次做数据库的容灾演练,DBA 直接在生产库上按了断电按钮(当然是演练环境)。我心想:正在写一半的数据,这下肯定坏了吧?结果重启之后,数据库花了几秒"恢复",数据一条没丢,那些写了一半没提交的事务也干干净净地回滚了。
我问 DBA:断电那一刻,内存里的数据不是全没了吗?他说了一句我记到现在的话:"内存里的数据丢了没关系,因为真正算数的东西,早就写到日志里了。"
那个"真正算数的东西",就是数据库的日志 ,以及它背后一条铁律------WAL(Write-Ahead Logging,先写日志)。这篇文章就把这套机制讲清楚。
为什么不能"直接改数据"
最直觉的做法是:事务要改哪条数据,就直接在磁盘上改哪条。可这里有个致命矛盾:
- 改到一半断电了怎么办?------数据可能"改了一半",既不是旧值也不是新值,直接损坏;
- 事务改了但没提交,改到一半又回滚了怎么办?------磁盘上已经被改花,回滚还得再写回去,既慢又危险。
于是数据库想了个办法:把"我要做什么"先记在日志里,日志写稳了,再去动数据。 日志是顺序追加的、结构简单、写起来快,而且能完整记录"改前长什么样、改后长什么样"。
WAL:先写日志,再改数据
WAL 协议的核心就一句话:在对数据页做任何修改之前,必须先把对应的日志记录落盘(flush 到磁盘)。
text
1. 事务要修改数据
2. 先把"改前值 + 改后值"写成一条日志,落盘
3. 日志落盘成功,才允许修改内存/磁盘上的数据
4. 提交时,再落一条"提交"日志
为什么要"先"?因为日志是顺序写 (append-only),数据页是随机写 ,顺序写比随机写快得多。把慢的随机写"延迟"到后面批量做,把快的顺序写"提前"做,既保证安全又提升性能。日志就是那本"先记账、后发货"的账本。
redo 与 undo:两本账,管两件事
日志里通常记两类信息,对应两种恢复动作:
- redo 日志 :记"改后值"(新值)。作用是重做 ------崩溃后,把"已经提交、但还没来得及写进数据页"的修改,照着日志再执行一遍,保证提交了的一定不丢。
- undo 日志 :记"改前值"(旧值)。作用是撤销 ------崩溃后,把"没提交、却已经把修改写进去了"的事务,照着日志回滚掉,保证没提交的一定不生效。
一句话分工:
text
redo → 提交了的,一定让它生效(重做)
undo → 没提交的,一定让它消失(撤销)
两者配合,就能在崩溃后把数据库恢复到一个"所有已提交事务生效、所有未提交事务回滚"的一致状态。
崩溃恢复:重启后那几秒在干什么
你看到的"正在恢复",其实就是数据库在重放日志:
text
1. 扫描日志,找到最后一个检查点(checkpoint,定期把数据刷盘并记录位置)
2. 从检查点往后,收集所有已提交、未提交的事务
3. 对已提交的 → 重做 redo(把修改补上)
4. 对未提交的 → 回滚 undo(把修改撤销)
5. 数据页和数据一致了,恢复完成
检查点(checkpoint) 的作用是"缩短恢复时间":没有它,每次崩溃都得从头扫日志;有了它,只需从最近一个检查点往后重放,恢复快得多。
为什么这套设计这么重要
它同时解决了三个看起来矛盾的需求:
- 性能:先顺序写日志,把慢的随机写数据页延迟、合并,事务吞吐大幅提升;
- 原子性:崩溃后靠 undo 把"半截子事务"撤销干净;
- 持久性:崩溃后靠 redo 把"已提交事务"找补回来。
这三条合起来,正是事务 ACID 里的 A(原子性)和 D(持久性) 的工程实现。所以 WAL 几乎是所有主流数据库(MySQL InnoDB、PostgreSQL、SQLite)的标配,也顺带催生了"日志即数据"这一整套现代存储思想。
所以下次再看到数据库启动时那一行"正在恢复(recovery)",别慌------它不是坏了,而是在把账本重新过一遍,保证"说过的每一笔都算数"。
数据库的日志与恢复,和操作系统、文件系统里的"日志/回放"思路一脉相承,属于理解存储可靠性的通用视角。