上节使用 undo 日志完成了系统故障后的数据恢复,但 undo 日志有一个限制,即在将事务对数据的修改刷写到磁盘前不能提交该事务。这样做可能会导致事务因为等待磁盘 IO 而被阻塞,也会占用较多的磁盘带宽。此外,由于每次故障恢复都要把相关数据恢复到事务之前的状态,即便这些数据已经部分或全部更新到磁盘了,这样反复修改依然浪费了很多资源。
事实上只要有日志用于恢复数据,那么把修改的数据暂存在主存中是安全的,并非一定要马上同步到磁盘。redo(重做)日志就是这样一种日志,它不需要等待数据写到磁盘就可以提交事务。
redo 日志与 undo 日志的区别如下:
-
undo 日志在恢复时撤销未完成事务的操作并忽略已提交事务,而 redo 日志忽略未完成事务并重做已提交的事务。
-
undo 日志要求先将修改后的数据写到磁盘再写入 COMMIT 日志记录,而 redo 日志要求先写入 COMMIT 日志记录再将修改后的数据写到磁盘。
-
在使用 undo 日志时,记录的是数据库元素的旧值,而使用 redo 日志时,记录的是数据库元素的新值。
在 redo 日志中,日志记录 [T, X, v] 的含义是“事务 T 将数据库元素 X 的值修改为 v”。每当一个事务 T 修改一个数据库元素 X 时,必须往日志中写入一条形如 [T, X, v] 的记录。redo 日志需要遵循如下规则:
R1:在修改磁盘上任何数据库元素 X 以前,要保证更新记录 [T, X, v] 以及事务提交记录 [COMMIT T] 都已经写到磁盘上。
【例 5.6】考虑与例 5.2 中相同的事务 T,表 5-6 给出了该事务一个可能的事务序列。
表 5-6
步骤 | 操作 | t | A(内存) | B(内存) | A(磁盘) | B(磁盘) | 日志 |
---|---|---|---|---|---|---|---|
1 | [START T] | ||||||
2 | READ(A, t) | 15 | 15 | 15 | 15 | ||
3 | t := t-10 | 5 | 15 | 15 | 15 | ||
4 | WRITE(A, t) | 5 | 5 | 15 | 15 | [T, A, 5] | |
5 | READ(B, t) | 15 | 5 | 15 | 15 | 15 | |
6 | t := t+10 | 25 | 5 | 15 | 15 | 15 | |
7 | WRITE(B, t) | 25 | 5 | 25 | 15 | 15 | [T, B, 25] |
8 | [COMMIT T] | ||||||
9 | FLUSH LOG | ||||||
10 | OUTPUT(A) | 25 | 5 | 25 | 5 | 15 | |
11 | OUTPUT(B) | 25 | 5 | 25 | 5 | 25 |
表 5-6 与表 5-2 的主要区别有以下几点:
-
在第 4 行和第 7 行,表 5-6 中记录的是 A、B 的新值而不是旧值。
-
[COMMIT T] 记录出现得更早,只有先将日志刷写到磁盘,才能将 A 和 B 的新值更新到磁盘上。
-
在第 10 和第 11 行才将数据写入磁盘,实际上它们发生的时间可能更晚。
因为“先写日志规则”的存在,只要日志中没有 [COMMIT T] 记录,就可以知道事务 T 所做的修改没有写到磁盘上,在恢复时也不需要做任何处理。对于已提交的事务,记录了数据修改后的新值,所以可以恢复到事务完成后的状态。在系统故障后使用 redo 日志恢复时,需要进行以下工作:
-
确认已提交的事务。
-
从头开始扫描日志,对于每一个 [T, X, v] 记录:
a. 如果 T 是未提交的事务,则不需要处理。
b. 如果 T 是已提交的事务,则将数据库元素 X 的值改为 v(不管 X 的值是否已经是 v)。
-
对每个未完成的事务 T,在日志中写入一个 [ABORT T] 记录并刷写日志。
【例 5.7】考虑表 5-6 中的日志。
-
如果故障发生在第 9 步之后,那么 [COMMIT T] 记录已经被刷写到了磁盘。恢复管理器判定 T 是一个已提交的事务,继续扫描日志,日志记录 [T, A, 5] 和 [T, B, 25] 告诉恢复管理器将 A 的值修改为 5,将 B 的值修改为 25。如果故障发生在第 10 步或者第 11 步之后,那么写 A 或者写 B 是多余的,但也是无害的。
-
如果故障发生在第 8 步和第 9 步之间,那么 [COMMIT T] 记录已经写入了日志。如果该记录已到达磁盘,则像情况 1 那样进行恢复即可,而如果该记录没能到达磁盘,那么应该和下面的情况 3 一样。
-
如果故障发生在第 8 步以前,那么 [COMMIT T] 记录肯定没有到达磁盘,因此 T 被认为是一个未完成的事务。磁盘上的 A 和 B 不为 T 做任何改变,并且需要写一条 [ABORT T] 记录到日志中。
对于 redo 日志,事务对数据的修改到达磁盘的时间可能比事务提交的时间晚很多,所以不能仅考虑创建检查点时的活跃事务,还需要明确知道哪些缓冲区数据还没刷写到磁盘,以及哪些事务修改了哪些缓冲区。经过 5.3 undo 日志 的分析,已经知道了动态检查点效率更高,所以这里直接介绍 redo 日志的动态检查点机制。
为 redo 日志创建动态检查点的步骤如下:
-
写入日志记录 [START CKPT(T1, ..., Tk)] 并刷写日志,其中 T1, ..., Tk 是所有活跃(即未提交的)事务。
-
将当前所有已提交事务已经写到缓冲区但还没有写到磁盘的数据库元素写到磁盘。
-
写入日志记录 [END CKPT] 并刷写日志。
【例 5.8】表 5-7 给出了一个可能的 redo 日志,其中发生了一个检查点。当开始创建检查点时,只有事务 T2 是活跃的。T1 对 A 写入的新值可能已经到达磁盘,如果没有,那么必须在检查点结束前将 A 拷贝到磁盘。
检查点的结束在几个其他的事件后发生:T2 为数据库元素 C 写入一个值,一个新事务 T3 开始并为 D 写入一个值。在检查点结束后发生的唯一的事情是 T2 和 T3 的提交。
表 5-7
[START T1] |
---|
[T1, A, 10] |
[START T2] |
[COMMIT T1] |
[T2, B, 20] |
[START CKPT(T2)] |
[T2, C, 30] |
[START T3] |
[T3, D, 40] |
[END CKPT] |
[COMMIT T2] |
[COMMIT T3] |
正如 undo 日志那样,检查点可以帮助缩小需要恢复的日志范围。根据最后一条检查点记录是 START 还是 END,有两种不同的情况。
-
假设故障发生前最后一条检查点记录是 [END CKPT],对应的 [START CKPT(T1, ..., Tk)] 前提交的事务已经将其所做的修改写到了磁盘。因此只需考虑 T1, ..., Tk 等活跃事务以及检查点之后开始的新事务,必须要将这些事务全部像 使用 redo 日志恢复数据 中那样进行恢复。
在扫描日志时,当找到最早的已提交事务 Ti 的 [START Ti] 记录后就不必继续扫描,因为之前的日志都已经确认提交并刷写到磁盘。由于这些 START 记录可能出现在任意多个检查点前,就可以将所有日志记录链接在一起,以便更快地找到所需记录。
-
假设日志中最后一条检查点记录是 [START CKPT(T1, ..., Tk)],此时不能确定在此检查点开始前提交的事务是否已经将其修改写到磁盘。因此必须要索引到前一条 [START CKPT(S1, ..., Sn)] 记录,并重做这些已经提交的事务,包括某些 Si 以及检查点后新开始并提交的事务。
【例 5.9】重新考虑表 5-7 中的日志。
-
如果故障发生在日志末尾,从后往前搜索,找到 [END CKPT] 记录。这时只需要知道写在 START CKPT 记录里的所有事务和检查点后开始的新事务,因此我们考虑{T2, T3},当找到 [COMMIT T2] 和 [COMMIT T3] 记录时,就知道它们都需要重做。继续扫描日志直到找到 [START T2] 记录,根据读到的 [T2, B, 20]、[T2, C, 30]、[T3, D, 40] 记录,将 B、C、D 的值修改为 20、30、40。
-
假设故障发生在 [COMMIT T2] 和 [COMMIT T3] 之间,整体恢复过程与上述过程类似。不同的是事务 T3 会被判定为未提交事务,不需要重做,而且恢复完成后需要写入一条 [ABORT T3] 日志记录。
-
假设故障发生在 [END CKPT] 记录之前。原则上需要扫描到倒数第二个 START CKPT 记录,但在当前情况下前面没有检查点,就需要一直扫描到日志头部。最终可以确定已提交的事务为 T1,并在恢复后将记录 [ABORT T2] 和 [ABORT T3] 写入日志中。
由于事务可能在几个活跃点期间都是活跃的,因此 START CKPT 记录不仅包含事务的名字,还可以包含事务的起始地址,方便恢复时查找日志。而且当写入 [END CKPT] 记录时,就会知道最早开始的活跃事务 Ti 之前的日志记录都可以删除,因为它们对数据的修改在生成 [END CKPT] 记录之前就已经刷写到磁盘了。