what
Redo Log与Undo Log是系统恢复的基础前提
系统恢复时,undo需要redo的配合来实现。
redo日志有commit或者abort记录时,事务是无需undo的。
Redo Log
what
Redo Log以顺序附加的形式记录新值。
幂等。
why
Redo用来保证事务的原子性和持久性。
how
记录<T,X,V>,表示事物T将新值V存储到数据库元素X。
新值可以保证重做;
一个事务从开始到结束,要么提交完成,要么中止,具有原子性。
事务无关性,根据日志统一redo,之后的撤销工作交给undo来进行。
Undo Log
what
Undo记录通常以随机操作的形式记录旧值。
逻辑日志,并不幂等。
撤销时,根据undo记录进行补偿操作。undo本身也产生redo记录。通过undo日志数据库可以实现MVCC(Multi-Version Concurrency Control 多版本并发控制)。保证了事务失败或者主动abort时的机能,系统崩溃恢复时,也确保数据库状态能恢复到一致。
why
Undo能保证事务的一致性。
how
记录<T1,Y,9>,表示事物T1对Y进行了修改,修改前Y的值是9。
旧值能用于撤销,也能供其他事务读取。
CheckPoint检查点
what
分类
简单检查点
停止接受新的事务->等待当前所有活跃事务完成或中止,并在日志中写入commit或abort记录->当前位于内存的日志,将缓冲块缓存到磁盘->写入日志记录<CKPT>,再次刷新到磁盘。->重新开始接受事务,系统恢复。
从日志尾端反向搜索,直到找到第一个<CKPT>标志并处理。
非静止检查点
检查过程中允许接受新事物进入。
将一个点扩展到一个处理区间。(JVM的GC处理从Stop the world 到安全区的处理)
写入日志记录<START CKPT(t1,...tn)>,其中t1,...tn所有的事务都已完成,写入日志记录<END CKPT>
从日志尾端反向搜索。
先遇到<START CKPT(t1,...tn)> 系统在检查点过程中崩溃
未完成事务的部分:
(t1,...t2)记录的部分以及<START>标志后新进入的部分。
这部分事务最早的那个事务的开始点就是扫描的截止点,再往前可以不必扫描。
先遇到<END CKPT> 系统完成了上一个周期的检查点,新的检查点还没开始。
需要处理的事务:
<END CKPT>标志之后到系统崩溃这段时间内的事务以及上一个<START>,<END>区间内新接受的事务。
为此,扫描到上一个检查点<START CKPT()>就可以截止。
why
日志很长时,搜索过程太耗时。
redo是幂等的,大多数需要重做的事务已经把更新写入,恢复过程会变得很长。
一旦事务commit日志记录写入磁盘,逻辑上而言,本事务的undo记录在恢复时已经不需要,在commit时可以删除之前的undo记录。但由于多事务同时执行,其他事务可能仍在使用undo中的旧值,因此需要checkPoint来处理这些当前活跃的事务。
why
how
写日志
直接写入到存储介质
等待一次IO。
fsync函数。
先写到缓存,在之后的某一时间点统一写入磁盘
存储数据
方案一
操作流程简单如下(假设每次数据变化,都提交):
- 更新的操作方式依次记录到磁盘日志文件。(如果在写操作作日志的时候发生故障,那么这次数据库操作失败)
- 更新内存中的数据。
- 返回更新成功结果。
恢复流程:
- 读取日志文件,依次修改内存中的数据。
优点:
- 日志文件有序,可以通过append的方式写入磁盘,性能很高。
- 简单可靠,应用广泛。可以把内存中的数据,做备份在磁盘中。
缺点:
- 使用时间一长,恢复宕机的时间很慢。
方案二
操作流程:
- 日志文件记录
begin check point
- 在某个时刻,把内存中的数值,直接snapshot或dump到磁盘上。(比如直接记录a=4)
- 日志文件记录
end check point
恢复流程:
- 扫描日志文件,找到最后的
end check point
中配对的begin check point
。 - 读入dump文件。
- 依次回放记录的日志操作。
优点:
- 应用广泛,包括 mysql,oracle。
一些棘手的问题:
- 在做snapshot的时候,往往不能停止数据库的服务,那么很可能记录了
begin check point
之后的日志。 - 那么在重新
load begin check point
之后的日志时,最后恢复的数据很有可能不对。比如记录的是a++这样的日志, 那么重复一条日志,就会让a的值加1。反之如果记录是幂等的,比如一直是 a=5 这种操作,那么就对最后结果没有影响。
很显然,设计幂等操作系统很麻烦。
- 设计一个支持snapshot的内存数据结构,也比较麻烦。
典型的是通过copy-on-write
机制。和操作系统中的概念一样。当这个数据结构被修改,就创建一份真正的copy。老数据增加一份dirty flag。如果没有修改就继续使用之前的内存。这样在做snapshot的时候,保证我们的dump数据是begin check point
这个时刻的数据。显然这个也比较麻烦。
还有一种支持snapshot的思路是begin check point
后,不动老的数据。内存中的数据在新的地方,日志也写在新的地方。最后在end check point
做一次merge。这个实现起来简单,但是内存消耗不小。