本页目录

数据库 III · 事务、MVCC 与恢复

对标:CMU 15-445 后半 / Database Internals 事务篇 | 前置:db-01/02、os-02(并发)、os-03(崩溃一致性/日志) 数据库最深刻的部分——事务。它给你一个惊人的承诺:一组操作要么全成功要么全不发生(原子),并发执行像串行一样正确(隔离),一旦提交断电也不丢(持久)。这一页讲清 ACID 怎么实现:并发控制(锁 vs MVCC,Postgres 用的后者)与崩溃恢复(WAL——你会看到它就是 os-03 文件系统日志的直系后代)。

1. ACID 与并发的威胁

ACID原子性(Atomicity,全做或全不做)、一致性(Consistency,不破坏约束)、隔离性(Isolation,并发如串行)、持久性(Durability,提交不丢)。前三个防并发与故障,最后一个防断电。

并发事务不控制会怎样(隔离性要消灭的异常):

隔离级别是"允许哪些异常换多少并发"的旋钮:读未提交 < 读已提交 < 可重复读 < 可串行化(最严,完全等价某个串行顺序,无任何异常)。级别越高越安全越慢——又一个 CS 里无处不在的权衡。Postgres 默认"读已提交",Medusa 若有并发写要想清楚够不够。

2. 可串行化:正确性的黄金标准

冲突图无环⟺可串行化(拓扑排序)。

图 db-03.3冲突图无环⟺可串行化(拓扑排序)。

并发调度正确的定义:可串行化——并发执行的结果等价于某个串行执行顺序。怎么判一个调度是否可串行化?冲突图(优先图):事务为点,冲突操作(对同一数据的读写/写写)定向连边,图无环 ⟺ 可串行化(🔗 algo-01 的拓扑排序——无环才有合法串行顺序)。这把"并发正确性"化成一个优雅的图论判定。

3. 两条并发控制路线

MVCC 多版本:更新造新版本、读读快照、旧版本待 VACUUM 回收。

图 db-03.2MVCC 多版本:更新造新版本、读读快照、旧版本待 VACUUM 回收。

① 两阶段锁(2PL,悲观):读加共享锁、写加排他锁;所有加锁在放锁之前完成(增长阶段 → 收缩阶段)——可证明产生可串行化调度。代价:锁争用、死锁(os-02 的四条件在此,数据库用等待图检测死锁并回滚一个事务打破环)。

② 多版本并发控制(MVCC,乐观读,Postgres/你在用的)【机理级】每次更新不覆盖旧值,而是造一个新版本,每个版本带事务时间戳。读操作读"对我可见的那个版本"——读永不阻塞写、写永不阻塞读。这是 MVCC 的杀手锏:分析型长查询和写入可以并行不打架(对 Medusa 这种"一边写入一边读取分析"的负载极友好)。

4. 崩溃恢复:WAL(os-03 日志的直系后代)

WAL:先写日志→提交→崩溃后 redo 已提交/undo 未提交。

图 db-03.1WAL:先写日志→提交→崩溃后 redo 已提交/undo 未提交。

问题:事务提交了但脏页还在缓冲池没落盘,此刻断电——怎么保证已提交的不丢(D)、未提交的不残留(A)?答案是预写日志 WAL(Write-Ahead Logging),和 os-03 文件系统日志是同一思想

WAL 铁律任何数据页写回磁盘之前,描述这次修改的日志必须先落盘(write-ahead)。日志是顺序追加的(快),记录每次修改的 redo(如何重做)和 undo(如何撤销)信息。

ARIES 恢复三阶段【骨架】:崩溃重启后——

  1. 分析:扫日志,确定崩溃时哪些事务未完成、哪些脏页。
  2. Redo(重做):从日志重放所有修改(包括未提交的),把数据库恢复到崩溃那一刻的精确状态。
  3. Undo(撤销):回滚所有未提交事务的修改(用 undo 信息)。

结果:已提交的事务其修改一定在(D,靠 redo)、未提交的一定没有(A,靠 undo)。"先写日志、崩溃后重放 redo 撤销 undo"——这套机制让数据库能在任意时刻断电而不损坏。提交的定义就是"commit 日志记录落盘"那一刻(所以提交要 fsync,慢但可靠,🔗 os-03 "写入 ≠ 持久化")。

这条主线的完整闭环:os-03 文件系统日志 → 本页数据库 WAL → dist-02 分布式的复制日志(Raft log)——"把状态变更写成一条不可变的顺序日志"是可靠系统的元思想,本站三处呼应,你会在 dist 线看到它第三次登场。

5. 练习与要点

例 1(判可串行化) 给两个交错事务的操作序列,画冲突图判有无环——"并发对不对"用拓扑排序回答,把 algo 线的图论用到事务。

例 2(MVCC 为什么要 VACUUM) 描述一次 UPDATE 在 MVCC 下产生了什么(旧版本 + 新版本),解释不 VACUUM 会怎样(表膨胀、查询变慢)——把你 Medusa 运维里的 VACUUM 从"照做的命令"变成"懂原理的操作"

例 3(WAL 恢复推演) 事务 T1 已提交、T2 未提交时断电,脏页均未落盘——推演 redo/undo 各恢复了什么,验证 A 和 D 都满足。手推一遍,"数据库为什么断电不坏"就刻进去了\(\blacksquare\)


📋 大 Project P02(第三阶段·收官)· 事务与恢复

P02-C · ACID、MVCC 与 WAL(承接 P02-A/B):

  • 学习目标:让学生体验“正确的数据库不是能查数据,而是在并发和崩溃下仍然不撒谎”。
  • 教师提供:事务 API 骨架、并发事务调度器、崩溃注入 harness(在指定日志序号后 kill 进程)、隔离异常测试样例。
  • 学生任务:① 选择 MVCC 快照隔离或 2PL 可串行化路线,并在报告中说明取舍;② 实现事务开始/提交/回滚、版本可见性或锁管理、死锁检测/写冲突处理;③ 实现 WAL,保证 commit record 落盘后 redo/undo 恢复正确。
  • 正确性约束:提交点必须有明确定义;任何数据页刷盘前对应日志必须已刷;并发读写不得出现脏读、丢失更新;回滚必须清理索引与数据版本的一致性。
  • 验收测试:隔离级别测试通过;崩溃发生在“写日志前/写日志后/提交后/脏页刷盘后”的多种时刻,重启后已提交在、未提交无;长事务读快照期间并发写不破坏可见性。
  • 评分重点:事务语义 25%,并发控制 30%,WAL 恢复 30%,故障注入报告 15%。
  • P02 总结报告:要求学生用一条 SQL 从解析、计划、索引访问、事务提交、WAL 落盘到崩溃恢复画出端到端路径。

数据库三页完成。下一页进入分布式 I——多台机器协作的世界:时钟、一致性模型,以及 CAP 定理为什么让你必须取舍。