Skip to content

Latest commit

 

History

History
111 lines (68 loc) · 8.08 KB

6.undo-redo-log.md

File metadata and controls

111 lines (68 loc) · 8.08 KB

5.5 undo/redo 日志

前文介绍了两种恢复日志,它们的主要区别在于写日志时保存数据库元素的新值还是旧值,两种方式各有优劣:

  1. undo 日志要求在事务提交前把数据写到磁盘,可能增加磁盘 IO 次数。

  2. redo 日志要求在事务提交和日志刷写前保留所有的数据块缓存,这样会增加事务需要的缓冲区空间。

  3. 如果一个缓存块包含多个数据库元素,那么 undo 日志和 redo 日志在处理缓存区时都存在冲突。例如同一缓冲块包含一个已提交事务的元素 A 和一个未提交事务的元素 B,那么需要将 A 刷写到磁盘,但是 B 又要求不能这样做。

为了解决上述问题,引入了一种称为 undo/redo 日志的日志类型,它通过维护更多的信息来提供日志的灵活性。

undo/redo 日志规则

undo/redo 日志与前两种日志类型基本相同,但在修改数据库元素时写入的日志记录有四个部分。一条 [T, X, v, w] 记录表示事务 T 将数据库元素 X 的值从 v 修改为 w。undo/redo 日志需要遵循以下原则:

UR1:在事务 T 对数据库元素 X 的修改到达磁盘前,日志记录 [T, X, v, w] 必须刷写到磁盘上。

【例 5.10】表 5-8 是例 5.6 中事务 T 的一个变体,其中日志内容和事务操作顺序发生了变化。一条更新日志记录同时包含数据库元素的旧值和新值,[COMMIT T] 可以出现在第 7 步之后的任意位置。

表 5-8

步骤 操作 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, 15, 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, 15, 25]
8 FLUSH LOG
9 OUTPUT(A) 25 5 25 5 15
10 [COMMIT T]
11 OUTPUT(B) 25 5 25 5 25

使用 undo/redo 日志恢复数据

undo/redo 日志的恢复策略如下:

  1. 按照从前往后的顺序,重做所有已提交的事务。

  2. 按照从后往前的顺序,撤销所有未提交的事务。

【例 5.11】考虑表 5-8 中的动作序列。

  1. 假设故障发生在 [COMMIT T] 记录刷写到磁盘之后,这时 T 被认为是已提交的事务,需要为 A 和 B 写入日志记录中的新值。

  2. 假设故障发生在 [COMMIT T] 记录到达磁盘之前,则 T 被认为是未完成的事务,需要为 A 和 B 写入日志记录中的旧值。

和 undo 日志一样,使用 undo/redo 日志的系统中可能出现这样的行为:事务在用户看来已经提交(如在网上预订了一个航班座位然后断开连接),但由于 [COMMIT T] 记录尚未刷写到磁盘,后来的一次故障使该事务被撤销而不是重做。如果这样的可能性是一个问题,那么建议为 undo/redo 日志使用一条附加的规则:

UR2:[COMMIT T] 记录一旦出现在日志中就必须被刷写到磁盘上。

例如,在表 5-8 中我们将在第 10 步后加入“FLUSH LOG”。

undo/redo 日志的检查点

undo/redo 日志的动态检查点相对来说更为简单,创建检查点主要有以下几步:

  1. 写入日志记录 [START CKPT(T1, ..., Tk)] 并刷写日志,其中 T1, ..., Tk 是所有活跃的事务。

  2. 将缓冲区的所有脏数据写到磁盘。可以容忍将未完成事务修改的数据写到磁盘,所以能够容忍小于完整块的数据库元素,并因此可以在事务之间共享缓冲区。

  3. 写入日志记录 [END CKPT]。

【例 5.12】表 5-9 给出了类似于表 5-7 中 redo 日志的一个 un­do/redo 日志,区别在于更新记录的不同。

和例 5.8 中一样,T2 被确定为检查点开始时唯一的已提交事务。由于这一日志是 undo/redo 日志,有可能 T2 写入的 B 的新值 20 己写到磁盘,而这在 redo 日志中是不可能的。在检查点过程中,如果 B 的新值还不在磁盘上,因为会刷写所有的脏缓冲区,所以肯定会将 B 刷写到磁盘上。同样,如果由已提交的事务 T1 写入的 A 的新值还不在磁盘上,那么将刷写 A 到磁盘上。

表 5-9

[START T1]
[T1, A, 5, 10]
[START T2]
[COMMIT T1]
[T2, B, 10, 20]
[START CKPT(T2)]
[T2, C, 20, 30]
[START T3]
[T3, D, 30, 40]
[END CKPT]
[COMMIT T2]
[COMMIT T3]

如果系统故障在这一系列步骤的末尾发生,则 T2 和 T3 被判定为已提交的事务。事务 T1 在检查点前提交,由于在日志中发现 [END CKPT] 记录,可知 T1 已经完成并已将其改变写到磁盘上。因此只需要重做 T2 和 T3,就像在例 5.8 中那样。当重做像 T2 这样的事务时,并不需要查看比 [START CKPT(T2)] 还早的记录,因为 T2 在检查点开始前的改变在检查点过程中已经被刷写到磁盘。

假设故障正好在 [COMMIT T3] 记录写到磁盘前发生,那么可以判定 T2 是已提交的而 T3 是未提交的。可以通过将磁盘上的 C 置为 30 来重做 T2;没有必要将 B 置为 20,因为我们知道这一改变在 [END CKPT] 前已到达磁盘。和 redo 日志不同的是,这里还要撤销 T3,也就是将磁盘上的 D 置为 30。如果 T3 在检查点开始时是活跃的,将不得不查看比 START CKPT 记录更早的记录,以确定 T3 是否有更多的动作已到达磁盘因而需要撤销。

可以注意到,这里并没有指明在使用 undo/redo 日志恢复时先撤销还是先重做。事实上,不管先撤销还是先重做,都会面临如下情况:事务 T 提交了且被重做了。然而,T 读取了一个值 X,该值是由某个未提交并被撤销的事务 U 写入的。

问题不在于先重做还是先撤销,实际上数据库管理系统必须保证上述的情况绝不会发生。我们将在后续章节中讨论隔离像 T 和 U 这样的事务的方法,使它们能够并行地访问数据库元素 X 但不会相互影响。

undo/redo 日志的应用

这里介绍两个新的概念:steal 和 force。

  • steal/no-steal

    • 如果采用 steal 策略,那么数据库在事务提交之前就可以把修改后的数据写到磁盘,如果系统发生故障就可能导致数据库的不一致状态,因此数据库需要记录 undo log,在系统恢复时回滚未提交的事务。

    • 如果采用 no-steal 策略,在事务提交之前对数据的任何修改都不会到达磁盘,因此不需要记录 undo log,但这种方式限制了缓存的使用方式,导致不得不缓存大量的脏数据直到事务提交。

  • force/no-force

    • 如果采用 force 策略,在事务提交之前必须将修改的数据全部写到磁盘,这样做可以很好地保证数据库一致性,但会导致很多不必要的磁盘写入。

    • 如果采用 no-force 策略,可以在事务提交之后再把数据写入磁盘,但由于系统故障可能导致缓存中的数据丢失,所以需要记录 redo log,在系统恢复时重做已提交的事务。

目前数据库常用的就是 steal/no-force 策略,既允许在事务提交之前将脏数据写入磁盘,也允许在脏数据全部写入磁盘之前提交事务,当然这种策略需要 undo/redo 日志来进行故障恢复。不过,undo/redo 日志虽然能够带来较高的性能,但是也会增加数据库从故障中恢复的时间,所以并非在所有情况下都适用。