跳转至

事务、并发与恢复

1. 银行转账与 ACID

账户 A 向账户 B 转账时,扣款和入账必须在同一事务中完成:

1
2
3
4
BEGIN;
UPDATE account SET balance = balance - 1000 WHERE account_id = 'A001';
UPDATE account SET balance = balance + 1000 WHERE account_id = 'A002';
COMMIT;

任何一步失败都执行 ROLLBACK,不能保留“只扣款未入账”的中间结果。

  • 原子性:全部成功或全部失败。
  • 一致性:事务前后从一个合法状态到另一个合法状态。
  • 隔离性:并发事务不能看到不该看到的中间状态。
  • 持久性:提交后的修改在重启后仍保存。

2. 隔离级别与并发异常

两个转账请求同时读取余额并扣款,可能产生超额转出。常见隔离级别如下:

隔离级别 脏读 不可重复读 幻读
Read Uncommitted 可能 可能 可能
Read Committed 避免 可能 可能
Repeatable Read 避免 避免 一般仍可能
Serializable 避免 避免 避免

脏读是一个事务读取了另一个事务尚未提交的修改;如果该事务随后回滚,就读到了最终未生效的数据。不可重复读是同一事务两次读同一行结果不同;幻读是两次范围查询的行集合因新增或修改而不同。隔离越强,一致性保护越强,但并发性能通常越受影响。不同数据库的默认级别和对幻读的具体实现待核验。

3. 锁与死锁

共享锁 S 允许多个事务同时读;排他锁 X 用于修改并阻止冲突访问。转账扣款常先锁住账户记录,再检查余额和修改。两阶段锁协议要求事务先加锁,释放锁后不能再加锁。

死锁例:事务 1 持有 A 等待 B,事务 2 持有 B 等待 A。四个必要条件是互斥、请求并保持、不可剥夺、循环等待。可以通过破坏必要条件预防,或采用超时、检测与恢复等策略;具体数据库的锁粒度和死锁选择规则待核验。

4. 日志、WAL 与检查点

Redo Log 记录修改后的信息,用于重做已提交事务,主要保障持久性;Undo Log 记录修改前的值或反向操作,用于回滚未提交事务和读取旧版本,主要保障原子性。WAL(Write-Ahead Logging)要求修改数据页前先将对应日志写入并落盘。检查点定期刷盘并推进日志位置,缩短故障恢复时间。

若转账在扣款后、入账前断电,重启时数据库依据日志对已提交事务 redo、未提交事务 undo,避免留下半成品。SELECT 不写入用于故障恢复的事务日志;审计日志可记录谁在何时执行了什么 SQL(包括 SELECT),通用查询日志用于调试,慢查询日志用于性能分析,三者用途不同。

日志名称、持久化时机和 MVCC 的具体实现依数据库产品而异,教材口径待核验。

故障分类与恢复目标。 事务故障是单个事务逻辑错误、约束冲突或主动回滚;系统故障是断电、操作系统/DBMS 崩溃,内存内容丢失但磁盘通常仍在;介质故障是磁盘损坏、文件页不可读等导致持久数据丢失;通信故障是分布式环境网络中断或消息丢失,可能造成状态不确定。机制上先确认故障范围和最后可靠副本,再恢复数据并重放/回滚日志,最后校验事务一致性。场景:SQL 约束错误回滚属事务故障,主机掉电属系统故障,存储设备损坏属介质故障。易错边界:故障类型按损坏范围区分;崩溃恢复不能替代介质备份,备份不能自动解决未完成事务的原子性。

备份与恢复策略。 全量备份复制选定范围的完整数据;增量备份保存自上次备份后的变化;差异备份保存自最近一次全量备份后的变化;日志备份保留事务日志以支持时间点恢复(具体能力依 DBMS)。恢复步骤:选取可用全量备份→按所需顺序应用差异/增量→重放日志至目标时间点→执行一致性检查→切换服务并记录恢复点。场景:误删关键表后恢复到误操作前,或介质损坏后由备份恢复数据库。易错边界:增量链依赖每个前序增量,差异通常只依赖最近全量;备份“成功”不等于“可恢复”,需定期演练;RPO 表示可接受的数据丢失时间窗口,RTO 表示可接受的恢复服务时间,不能互换。

日志结构与 WAL。 典型更新日志至少记录事务标识、数据项/页、旧值和新值(物理、逻辑或生理日志的细节不同),可供撤销和重做。WAL 的关键约束是:对应更新日志记录先于脏数据页写入稳定存储;事务提交确认前,保证提交所需日志已持久化。机制:更新内存页并产生日志→日志先刷盘→数据页可稍后刷盘→崩溃后依据日志恢复。场景:已提交的交易数据页尚未写回磁盘时断电,可由日志重做。易错边界:WAL 不代表每次修改都立即刷数据页;日志必须先于数据页,不能反过来;不同产品可能同时存在 undo/redo、重做日志、回滚段或版本链,不要把名称混为同一物理文件。

检查点。 检查点是恢复优化机制,记录某一时刻需要关注的活动事务/脏页和日志位置,并推动部分脏页写回。步骤:触发检查点→记录检查点信息→按策略刷写或标记未完成工作→崩溃恢复从相关位置开始分析,而非无条件扫描全部历史日志。场景:运行数日的数据库在重启后缩短日志扫描范围。易错边界:检查点不是备份,也不保证此刻所有事务都已提交或所有数据都已刷盘;模糊/增量检查点和恢复算法因系统而异。

Undo/Redo 恢复链。 以日志中的事务状态和更新记录为依据,通常先分析故障时活动事务与日志边界,再重做应保留的更新,最后撤销未提交事务;经典教材算法可能按“从最近检查点确定活动事务→已提交事务 REDO→未提交事务 UNDO”描述,具体扫描顺序取决于日志格式和恢复算法。若日志保存前后镜像,REDO 把数据恢复到新值,UNDO 恢复旧值;重做/撤销操作需具备幂等或可判定的页状态。场景:T1 已提交、T2 未提交后崩溃,恢复要保证 T1 的效果存在且 T2 的效果消失。易错边界:不能说“所有事务一律重做”或“一律撤销”;判据是提交状态与日志协议;提交后页面未落盘要 REDO,未提交写入即使已落盘也要 UNDO;部分现代系统采用 ARIES 等算法,阶段顺序和补偿日志更精细。

端到端恢复流程对比。 事务回滚处理运行中的逻辑失败;崩溃恢复借助本机日志处理系统故障;备份恢复应对数据文件丢失或损坏;灾难恢复还需异地副本、网络、应用和切换演练。考题常把“恢复”混为一谈,答题应先说明故障类型,再选择日志、备份或容灾手段,并说明 REDO/UNDO 的对象和目标。练习:T1 提交后数据页未刷盘、T2 更新已刷盘但未提交,断电后应如何处理?答案: 对 T1 REDO,对 T2 UNDO。解析: 是否已提交决定事务效果是否保留,数据页是否落盘不改变事务的提交状态;WAL 与日志记录为此提供依据。

完整样例:逐记录日志恢复(教材前后镜像模型)。 本例是可手工复算的系统崩溃模型,不是某产品的实际日志格式。假设允许未提交脏页落盘(steal),提交不强制刷全部数据页(no-force),更新日志含前后镜像、遵守WAL,提交确认前提交记录已持久化。起点是无活动事务、数据已完整落盘的简化静止检查点,A=100、B=200。之后稳定日志如下,单位为元:

LSN 日志记录 旧值 新值
0 CHECKPOINT:无活动事务,A=100、B=200已落盘 — —
1 BEGIN T1 — —
2 UPDATE T1 A 100 80
3 COMMIT T1(已落盘) — —
4 BEGIN T2 — —
5 UPDATE T2 B 200 250
6 UPDATE T2 B 250 260
— CRASH:无T2提交记录 — —

崩溃磁盘状态为A=100、B=260:T1页尚未刷回,T2页在LSN6日志先落盘后已刷回。预期恢复步骤和结果:

步骤 对象与动作 A B
分析 从检查点识别T1已提交、T2未提交 100 260
REDO 按LSN递增重做T1的LSN2,写入新值80 80 260
UNDO 按LSN递减撤销T2的LSN6,恢复旧值250 80 250
UNDO 撤销T2的LSN5,恢复旧值200 80 200
完成 记录撤销完成并持久化恢复结果 80 200

解析:T1已提交故其效果必须保留,即使原数据页尚未落盘;T2未提交故两次更新都撤销,不能只撤销最后一条。反向撤销才能让260→250→200,顺序反了会错误停在250。教材镜像赋值可重做同一记录,但恢复过程再次崩溃时仍须用持久化恢复进度/补偿日志保证可重入,不能无状态地重复任意步骤。现代ARIES可能先重复历史(包含未提交更新)再撤销并写补偿日志,具体阶段不等同本简化算法,事务最终目标仍为A=80、B=200。介质损坏须另用备份和可用日志;本例没有模拟真实数据库断电。