第九章 恢复系统
9.1 故障分类
| 故障类型 | 描述 |
|---|---|
| 事务故障 | 逻辑错误:事务因内部错误条件无法完成;系统错误:DBMS 因错误条件(如死锁)必须终止活跃事务 |
| 系统崩溃 | 电源故障或其他硬件/软件故障导致系统崩溃。Fail-stop 假设:非易失性存储内容不被系统崩溃破坏(数据库系统有大量完整性检查来防止磁盘数据损坏) |
| 磁盘故障 | 磁头碰撞或类似磁盘故障导致全部或部分磁盘存储损坏。损坏是可检测的(磁盘驱动器使用校验和检测故障) |
9.2 恢复算法
恢复算法包含两个部分:
| 部分 | 内容 |
|---|---|
| 正常运行期间 | 确保有足够信息来从故障中恢复的操作 |
| 故障发生后 | 将数据库恢复到保证原子性、一致性、持久性的状态的操作 |
核心问题:转账事务修改 A 和 B,若故障发生在一次修改后、另一次修改前:
- 修改了数据库但事务未提交 → 数据库不一致
- 事务已提交但未修改数据库 →更新丢失
9.3 存储结构
| 存储类型 | 特点 | 示例 |
|---|---|---|
| 易失性存储 | 系统崩溃后不保留数据 | 主存、高速缓存 |
| 非易失性存储 | 系统崩溃后保留数据,但自身可能故障 | 磁盘、磁带、闪存、NVRAM |
| 稳定存储 | 理论上的"永不故障"存储 | 通过在不同非易失介质上维护多个副本近似实现 |
9.3.1 稳定存储的实现
策略:在独立磁盘上维护每个块的多个副本(可在远程站点保护火灾/洪水等灾难)。
数据传输中的故障防护(假设每个块有两个副本):
- 将信息写入第一个物理块
- 第一次写入成功后,将相同信息写入第二个物理块
- 仅当第二次写入成功完成后,输出操作才算完成
副本不一致的恢复:
- 查找不一致块 → 在非易失性存储上记录进行中的磁盘写入,恢复时仅比较这些块(而非比较全部块)
- 若某副本校验和错误 → 用另一个副本覆盖;若两者校验和均正确但内容不同 → 用第一个副本覆盖第二个
9.4 数据访问
9.4.1 基本操作
| 概念 | 说明 |
|---|---|
| 物理块 | 磁盘上的块 |
| 缓冲区块 | 临时驻留在主存中的块 |
块移动操作:
| 操作 | 含义 |
|---|---|
input(B) | 将物理块 B 传输到主存 |
output(B) | 将缓冲区块 B 写入磁盘,替换对应物理块 |
假设:每个数据项恰好存储在一个块中。
9.4.2 事务的私有工作区
每个事务TiT_iTi有私有工作区,保存其访问和更新的所有数据项的本地副本。TiT_iTi对数据项 X 的本地副本记为xix_ixi。
| 操作 | 含义 |
|---|---|
read(X) | 将数据项 X 的值赋给本地变量xix_ixi |
write(X) | 将本地变量xix_ixi的值赋给缓冲区块中的数据项 X |
⚠️
output(B_X)不必紧跟在write(X)之后——系统可在适当时机执行。
访问规则:
- 首次访问 X之前必须执行
read(X)(后续读取可从本地副本) write(X)可在事务提交前任意时间执行
9.5 恢复与原子性
为确保故障下的原子性,首先将描述修改的信息输出到稳定存储,而不修改数据库本身。
核心方法:
- 日志恢复机制(重点)
- 影子副本/影子分页(较少使用,详见教材)
9.6 基于日志的恢复
9.6.1 日志记录
日志是日志记录的序列,记录数据库上的更新活动信息。日志保存在稳定存储上。
| 时机 | 日志记录 |
|---|---|
| 事务TiT_iTi开始 | <Ti start> |
TiT_iTi执行write(X)之前 | <Ti, X, V1, V2>(V1 = 旧值,V2 = 新值) |
| TiT_iTi完成最后一条语句 | <Ti commit> |
9.6.2 两种日志方法
| 方法 | 规则 | 特点 |
|---|---|---|
| 立即修改 | 未提交事务的更新可在事务提交前写入缓冲区或磁盘 | 更新日志记录必须先于数据库项写入;磁盘输出可随时发生,顺序可与写入顺序不同 |
| 延迟修改 | 仅在事务提交时才执行缓冲区/磁盘更新 | 简化恢复的某些方面,但需存储本地副本的开销 |
9.6.3 事务提交
事务在
<Ti commit>日志记录输出到稳定存储时被视为已提交。
- 该事务所有先前的日志记录必须已经输出
- 事务提交时其写入可能仍在缓冲区中,稍后再输出到磁盘
9.7 并发控制与恢复
在并发场景下:
- 所有事务共享单个磁盘缓冲区和单个日志
- 一个缓冲区块可能包含一个或多个事务更新的数据项
- 日志记录可交错写入
关键假设(严格 2PL 保证):
若事务TiT_iTi修改了某数据项,其他事务不能修改同一数据项,直到TiT_iTi提交或中止。
——否则若T1T_1T1更新 A →T2T_2T2更新 A 并提交 →T1T_1T1中止,如何进行 undo?
9.8 Undo 与 Redo 操作
9.8.1 Undo
undo(Ti):
- 逆向扫描TiT_iTi的日志记录,将所有被TiT_iTi更新的数据项恢复为其旧值
- 每次恢复时写出特殊日志记录
<Ti, X, V>(V = 旧值) - Undo 完成后写出
<Ti abort>
9.8.2 Redo
redo(Ti):
- 正向扫描TiT_iTi的日志记录,将所有被TiT_iTi更新的数据项设置为其新值
- 此过程不写日志
9.8.3 故障时的恢复决策
| 日志状态 | 操作 |
|---|---|
有<Ti start>,无<Ti commit>或<Ti abort> | UndoTiT_iTi |
有<Ti start>,且有<Ti commit>或<Ti abort> | RedoTiT_iTi |
9.8.4 重复历史
若TiT_iTi此前已 undo 并写出
<Ti abort>记录,然后发生故障——恢复时会redoTiT_iTi。
Redo 会重新执行TiT_iTi的所有原始操作——包括恢复旧值的步骤。这称为重复历史。看似浪费,但大大简化恢复逻辑。
立即修改恢复示例:
| 时间点 | 日志内容 | 恢复动作 |
|---|---|---|
| (a) 仅有 T0 的日志,无 commit | <T0 start>,<T0, A, 1000, 950>,<T0, B, 2000, 2050> | undo(T0):B→2000, A→1000,写<T0 abort> |
| (b) T0 已 commit,T1 未 commit | 含<T0 commit>+ T1 的 update 记录 | redo(T0) + undo(T1):A/B→950/2050,C→700,写<T1 abort> |
| © 两者均已 commit | 含<T0 commit>+<T1 commit> | redo(T0) + redo(T1):A/B→950/2050,C→600 |
9.9 检查点
9.9.1 为什么需要检查点
- 对整个日志中所有事务做 redo/undo极慢
- 系统长时间运行后处理整个日志耗时巨大
- 已将其更新输出到磁盘的事务会被不必要地 redo
通过周期性创建检查点来简化恢复过程。
9.9.2 检查点过程
- 将主存中所有日志记录输出到稳定存储
- 将所有已修改的缓冲区块输出到磁盘
- 将
<checkpoint L>日志记录写入稳定存储(L= 检查点时所有活跃事务的列表) - 创建检查点时停止所有更新
9.9.3 使用检查点的恢复
只需考虑在检查点之前开始的最近事务TiT_iTi,以及TiT_iTi之后开始的事务。
恢复流程:
- 从日志末尾逆向扫描,找到最近的
<checkpoint L>记录 - 仅 L 中的事务 + 检查点后开始的事务需要 redo/undo
- 检查点前已提交/中止的事务——其更新已全部输出到稳定存储,可忽略
- 继续逆向扫描直到找到 L 中每个事务的
<Ti start>记录 - 早于最早
<Ti start>的日志部分——恢复不需要,可任意擦除
检查点恢复示例:
| 事务 | 状态 | 恢复动作 |
|---|---|---|
| T1 | 检查点前已提交 | 可忽略(更新因检查点已输出到磁盘) |
| T2 | 检查点后开始,有 commit | Redo |
| T3 | 检查点后开始,有 commit | Redo |
| T4 | 检查点后开始,无 commit | Undo |
本章重点:故障三分法(事务/系统/磁盘)与 fail-stop 假设、三层存储模型(易失/非易失/稳定存储)及稳定存储的多副本实现、日志作为恢复的基石——
<Ti start>/<Ti, X, old, new>/<Ti commit>三类记录的含义与写入时机、立即修改 vs 延迟修改、undo(逆向恢复旧值)与 redo(正向设置新值)的算法、恢复判断准则(有 start 无 commit → undo,有 start 有 commit → redo)、重复历史策略、检查点的作用——大幅缩小恢复需扫描的日志范围 L。