当向存储系统写一个数据元素时,通常是先写入主存或者缓冲,然后再写入磁盘,如果系统在写入磁盘的时候,系统发生故障,当系统恢复后,需要再次从磁盘中读取此数据元素的时候,并不知道此时磁盘上所保存的数据元素是正确的还是错误的,Redo日志是一种应对此种故障的比较常用的故障恢复策略。为了确保一个数据元素的完整性,还需要借助事务这一概念,对于更新数据一个元素的redo日志,我们将其描述为“在一个事物T中写入数据元素A的新值x”,使用元组<T, A, x>表示。
一个事务的数据元素的更新操作的redo日志,可以分成三个原语
<BEGIN T> 开始事务T
<T, A, x> 事务T写入数据元素A的新值x,这里,数据元素可能是一个数据块,一个记录,或者一个关系
<COMMIT T> 提交事务T
第三个原语非常重要,通常认为,只有一个事务具有提交记录,表明这个事务被完整的执行,即,更新数据元素的操作已经执行成功。
在存储系统更新一个数据元素时,为了保证系统能从故障中恢复后,保证数据的完整性和一致性,采用下面的策略写redo日志到文件(非易失存储)。
事务T开始时,向redo日志写一个<BEGIN T>记录
当更新数据元素A之前(注意,此时数据元素可能位于磁盘,也可能位于内存中),先向redo日志文件中写一条<T, A, x>记录。
当<COMMIT T>写入redo日志后,此更新操作才被反应到内存或者数据元素A的实际存储磁盘之中。
当系统从故障中恢复后,从redo日志文件头部开始需要扫描redo日志,以恢复数据,如果事务T具有<COMMIT T>记录,则把<T, A, x>中的新值x写入磁盘,如果此事务没有<COMMIT T>纪录,我们认为此事务没有执行成功,不把<T, A, x>中的新值x写入磁盘,并且向此redo日志添加一个<ABORT T>记录,告诉系统,忽略此日志记录。这个过程叫重做或者重放,由于redo日志中保存的是一个数据元素的新值,因而,即使多次重做,和重做一次,在保证数据完整性和一致性的效果是相等的。
比如,从账户A转50元到账户B,一系列相关的操作和redo日志记录,以一个泳道图的方式,表示如下,
+----------------+-------+-------+----------------+
| OP | M-A | M-B | REDO LOG |
+----------------+-------+-------+----------------+
| | | | <BEGIN T1> | (1)
| READ(A, a) | 200 | | | (2)
| a = a - 50 | | | | (3)
| | | | <T1, A, 150> | (4)
| OUTPUT(A, a) | 150 | | | (5)
| READ(B, b) | | 200 | | (6)
| b = b + 50 | | | | (7)
| | | | <T1, B, 250> | (8)
| OUTPUT(B, b) | | 250 | | (9)
| | | | <COMMIT T1> | (10)
+----------------+-------+-------+----------------+
注:如果崩溃发生在步骤(10)之后,事务T1是完全可以被重做的,重做后,磁盘上的数据正是人们所期望的值。
为了避免恢复数据时,一直从头开始扫描redo日志文件,也为了避免redo日志文件长度永远不停增长,这里引入检查点这一个概念,日志管理器定期的检查redo日志,把日志记录中包含的新值写到其表示磁盘元素所应在的位置,对于系统恢复而言,可以忽略已经反映到磁盘中的事务redo日志记录
在检查redo日志的时候,一种简单的方式是检查点开始的时候,停止redo日志的更新,写入<START CKPT <t1, t2,,, tn>>, 其中,t1, t2,,, tn是当前活跃的事务,等到当前活跃的事务结束后,写入<END CKPT>记录。
<BEGIN T1>
<T1, A, 150>
<BEGIN T2>
<START CKPT T1, T2>
<T2, C, 100>
<T2, D, 200>
<COMMIT T1>
<COMMIT T2>
<END CKPT>
<BEGIN T3>
<T3, A, 100>
<COMMIT T3>
有了检查点后,在恢复数据时,不需要从redo日志的头部开始重做所有日志记录,只需要从日志文件的尾部开始向后扫描,直至遇到第一个<END CKPT>记录,此记录之前的redo日志由于已经被反映到磁盘上,因此不需要被再次重做,只需要重做此记录后的已提交的事务日志。
这种方式实现简单,但会造成日志管理在检查周期内不能增加新的事务,而影响整个系统的性能,一个在检查点内活动的、执行了很长时间的事务,会导致整个系统长时间不能接受新的事务。因此一种非静态检查点技术--不会阻塞检查点周期内日志更新,更适合实际使用,其遵循下面的规则:
检查点开始的时候,在日志文件中插入记录<START CKPT <t1, t2, ,, tn>>记录,其中,<t1, t2, ,, tn>是当前活动的事务。
在检查点周期内,如果此检查点所包含的活动事务全部提交,往日志文件尾部插入<END CKPT>记录,若没有全部提交,则无需要提交<END CPKT>记录,也无需等待,进入下一个检查点周期
<T1, A, 150>
<BEGIN T2>
<START CKPT, T1, T2> (1)
<BEGIN T3>
<T2, C, 100>
<START CKPT, T1, T2, T3> (2)
<T2, D, 200>
<COMMIT T1>
<START CKPT, T2, T3> (3)
<T3, A, 100>
<START CKPT T2, T3> (4)
<COMMIT T2>
<START CKPT T3> (5)
<COMMIT T3>
<BEGIN T4>
<END CKPT> (6)
<T4, B, 100>
<COMMIT T4>
...
为了说明非静态检查点的工作方式,我们让非静态检查点频繁的运行,在上面的例子中,检查点(1)开始时,有两个活动的事务,而在其开始后,系统启动了一个新的事务T3,检查点(1)结束后,T1和T2都未结束,根据规则,直接启动检查点(2),此时,有3个活动事务T1、T2和T3,当检查点(3)开始时,事务T1已经提交,因此,检查点(3)只检查事务T2和T3,但检查点(3)内,任何事务都没有提交,直到检查点(5)开始时,检查事务T3,随后系统新增加事务T4,检查点(5)之前,事务T3提交,因此,在日志文件插入<END CKPT>,在此之后,新的redo日志继续插入到日志文件。
从非静态检查点的redo日志恢复数据时,仍然从日志的尾部开始,向后扫描,在遇到<END CKPT>记录时,遵循下面的规则
继续后扫描,直到<START CKPT ... >记录处停止,然后从此处开始,向前重做redo日志
如果没有扫描到<END CKPT>记录,则无法知道哪些事务已经被反映到磁盘中,则从日志文件的开始处,重做所有的日志
如果所有重做操作都是成功的,向日志文件写入一条<END CKPT>记录
采用非静态检查点在恢复数据时会多扫描一些日志,但带来的好处是非常值得在实际应用中采用它。
由于向redo日志文件写redo日志时,可以采用只追加的方式,redo日志除了应对故障外,还可以带来一些额外的好处,当一个事务 的<commit T>记录写入日志文件后,更新操作<T, A, x>的新值可以先只反应到内存之中并且晚些时候把这个更新反应到磁盘之中,在把这些更新反应到磁盘中的时候,可以采用下面的策略,合并磁盘的操作
合并同若干个同时操作一个数据块的更新操作
采用一定的调度算法,按磁盘、磁片、磁道合并磁盘写操作
这 些更新操作只反应到内存而没有反应到磁盘之中的数据,被称为“胀数据”,当“胀数据”被驱赶出缓冲之前,“胀数据”包含对数据元素的更新需要立即被反映到 磁盘之中。如果存储系统设计在内存中维护了一个很大的数据缓冲,读取数据时,先从缓冲中读取,无疑会提高整个系统的性能。当然这里会引起另外一个话题,采用何种算法提高缓冲的命中率。
注:原创研究,若有转载,保留作者名称 @仪山湖