undo日志有一个潜在的问题,即我们在将书屋改变的所有数据写到磁盘前不能提交该事务。有时,如果让数据库修改暂时只存在于主存中,我们可以节省磁盘IO;只要在崩溃发生时有日志可以恢复,这样做就是安全的。
如果我们使用redo日志机制,立即将数据元素备份到磁盘的需要就可以被避免。redo日志和undo日志的主要区别是:
1. undo日志在恢复时消除未完成事务的影响并忽略已提交事务,而redo日志忽略未完成的事务并重复已提交事务所做的改变。
2. undo日志要求我们在COMMIT日志记录达到磁盘前将修改后的数据元素写到磁盘,而redo日志要求COMMIT记录在任何修改后的值到达磁盘前出现在磁盘上
3. 当遵循undo规则U1和U2时,在恢复时我们需要的是发生改变的数据源的旧值,而是用redo日志恢复时,我们需要的是新值。
规则
在redo日志中,日志记录<T, v="" x,="">的含义是“事务T为数据库元素X写入的新值v”。在这个记录中没有指出X的旧值。每当一个事务T修改一个数据元素X时,必须网日志中写入一条形如<T, v="" x,="">的记录。
对于redo日志,数据和日志项达到磁盘的顺序可以用一条“redo规则”描述,这条规则成为先写日志规则:
R1: 在修改磁盘上的任何数据元素X以前,要保证与X的这一修改先骨干的所有的日志记录,包括更新记录<T, v="" x,="">及记录,都必须出现在磁盘上。
事务的COMMIT记录只有在事务完成后才能写入日志,因而提交记录必然在所有更新日志记录后,所以当使用redo日志时,与一个事务相关的材料写入到磁盘的顺序为:
1. 指出被修改元素的日志记录。
2. COMMIT日志记录
3. 改变数据元素自身
下面我们按照undo日志规则来考虑如下的事务T:
A := A*
B := B*
| 步骤 | 动作 | t | M-A | M-B | D-A | D-B | 日志 |
|------+-------------+----+-----+-----+-----+-----+------------|
| ) | | | | | | | <START T> |
| ) | READ(A, t) | | | | | | |
| ) | t := t* | | | | | | |
| ) | WRITE(A, t) | | | | | | <T, A, > |
| ) | READ(B, t) | | | | | | |
| ) | t := t* | | | | | | |
| ) | WRITE(B, t) | | | | | | <T, B, > |
| ) | | | | | | | <COMMIT T> |
| ) | FLUSH LOG | | | | | | |
| ) | OUTPUT(A) | | | | | | |
| ) | OUTPUT(B) | | | | | | |
恢复
redo规则R1的一个重要推论是,只要日志中没有记录,我们就知道事务T对数据库所做的更新都没有写到磁盘上。因此,恢复时对未完成事务的处理就可以像它们从未发生似的。然而,已提交的事务存在问题,因为我们不知道他们的那些数据库改变已经写到磁盘。幸运的是,redo日志正好有我们需要的信息:新值。我们可以讲新值写到磁盘而不管他们是否已经在磁盘上。在系统崩溃后要使用redo日志恢复,我们需要做以下事情:
1. 确定已提交的事务。
2. 从收不开始扫描日志。对遇到的每一个<T, v="" x,="">记录:
a) 如果T是未提交的事务,则什么也不做
b)如果t是提交的事务,则为数据库元素X写入值v
3. 对每个未完成的事务T,在日志中写入一个记录并刷新日志。
我们考虑上图中的日志,看看在不同步骤上发生故障时如何进行恢复。
1. 如果故障发生在第9步的任何时候,那么记录已被刷新到磁盘。T是一个提交的事务。当向前扫描日志时,日志记录<T, 16="" a,="">和<T, 16="" b,="">将A和B写入16.请注意,如果故障发生在第10和11步之间,那么写A是多余的,而写B还未发生,因而将B改变为16是恢复数据库的一致状态所必须的。如果故障发生在第11步以后,那么些A和写B都是多余的但也是无害的。
2. 如果故障发生在第8和第9步之间,那么尽管记录写入了日志,但可能还没有到达磁盘(依赖于日志是否因其他原因而刷新)。如果该记录到达磁盘,则恢复如情况1那样进行,而如果该记录没能到达磁盘,那么恢复和下面的情况3一样。
3. 如果故障发生在第8步以前,那么记录肯定没有达到磁盘。因此,T被看做一个未完成的事务。磁盘上的A和B不为T做任何改变,而最后一条记录被写到日志中。
检查点
对于redo日志的检查点,这儿有一个undo日志中没有的问题。由于已提交事务做的数据修改拷贝到磁盘的时间可能比事务提交的时间晚的多,因此我们不能仅仅考虑在我们决定创建检查点时活跃的事务。在检查点的开始和结束之间我们必须将已被提交事务修改的所有数据库元素写到磁盘。
另一方面,我们不需要等待活跃事务提交或中止就能完成检查点,因为他们呢无论如何都不被允许在那个时候将它们写到磁盘。进行redo日志的非静止检查点步骤如下:
1. 写入日志记录,其中T1,...,Tk是所有活跃(即未提交的)事务,并刷新日志
2. 将START CKPT记录写入日志时所有已提交事务已经写到缓冲区但还没有写到磁盘的数据写到磁盘
3. 写入日志记录并刷新日志
例:下图给出了一个可能的redo日志,其中发生了一个检查点。当我们开始检查点时,只有T2是活跃的,但T1所写的A值可能已经到达磁盘。如果没有,那么我们必须在检查点结束前将A拷贝回磁盘。我们表明检查点的技术在几个其他的事件后发生:T2为数据库元素C写入一个值,一个新事务T3开始并为D写入一个值。在检查点结束后发生的唯一的事情是T2和T3提交。
<START T1>
<T1, A, >
<START T2>
<COMMIT T1>
<T2, B, >
<START CKPT(T2)>
<T2, C, >
<START T3>
<T3, D, >
<END CKPT>
<COMMIT T2>
<COMMIT T3>
使用带检查点的redo日志恢复
首先假设崩溃发生前日志中的最后一条检查点记录是。现在,我们知道在对应的前提交的事务已经将其修改写到了磁盘,因此我们不必关心如何恢复这些事务的影响。但是Ti中的任一事务或检查点开始后启动的任一事务及时已经提交,都仍然可能未将其修改转到磁盘上。因此,需要进行恢复,但可以只关心最后一个START CKPT(T1,...,Tk)中提到的事务Ti与该日志记录中在日志中出现后开始的事务。在搜索日志时,我们在找到最早的记录后就不必继续向后看。蛋清注意,这些START记录可能出现在任意多个检查点前。将一个事务的所有日志记录向后链接在一起可以帮助我们找到所需记录。
现在建设日志中的最后一条检查点记录是:
<START CKPT(T1,...,Tk)>
我们不能确定再次检查点开始前提交的事务是否已经将其修改写到磁盘上。因此我们必须搜索到前一记录,找到与之匹配的记录,并重做这些已经提交的事务,这些事务要么在START CKPT后开始要么在Si中。
例:考虑上图中的日志。如果故障在尾部发生,我们向后搜索,找到记录。我们只需知道将所有在写记录后开始的事务以及出现在该记录的列表中的事务(T2)作为重做的候选者。因此,我们的候选集合是{T2, T3}。我们找到了记录和,于是我们知道他们都必须重做。我们向后搜索日志直到记录,为提交的事务找到更新记录<T2, b,="" 10="">, <T2, 15="" c,="">, <T3, 20="" d,="">。由于我们不知道这些改变是否已经到达磁盘,我们分别为B、C、D重新写入值10、15、20.
现在我们假设崩溃在记录和之间发生。恢复过程与上述过程类似,只不过T3不再是已提交的事务,因此其修改<T3, 20="" d,="">不能被重做,并且在恢复中不对D做任何改变,尽管日志记录处于被检查记录范围内。恢复后我们也要在日志中写入一条记录。
最后,假设崩溃正好在记录前发生。原则上,我们必须i型昂后搜索到倒数第二个START CKPT记录并得到其活跃事务里诶包。但是,这种情况下没有前一个检查点,因为我们必须一致走到日志的开头。因此我们确定已提交的只有T1,重做其动作<T1, a,="" 5="">,并且在恢复后将记录和写入到日志中。