作为一个例子,我们将分析对于下面这组语句可能发生什么情况:
insert into t (x,y) values (1,1);
update t set x = x+1 where x = 1;
delete from t where x = 2;
我们会沿着不同的路径完成这个事务,从而得到以下问题的答案:
如果系统在处理这些语句的不同时间点上失败,会发生什么情况?
如果在某个时间点上ROLLBACK,会发生什么情况?
如果成功并COMMIT,会发生什么情况?
1. INSERT
对于第一条INSERT INTO T 语句,redo 和undo 都会生成。所生成的undo 信息足以使INSERT“消失
“。INSERT INTO T 生成的redo 信息则足以让这个插入”再次发生“。
插入发生后,系统状态如图9-1 所示。
这里缓存了一些已修改的undo 块、索引块和表数据块。这些块得到重做日志缓冲区中相应条目的“保
护“
假想场景:系统现在崩溃
即使系统现在崩溃也没有关系。SGA 会被清空,但是我们并不需要SGA 里的任何内容。重启动时就好
像这个事务从来没有发生过一样。没有将任何已修改的块刷新输出到磁盘,也没有任何redo 刷新输出到磁
盘。我们不需要这些undo 或redo 信息来实现实例失败恢复。
假想场景:缓冲区缓存现在已满
在这种情况下,DBWR 必须留出空间,要把已修改的块从缓存刷新输出。如果是这样,DBWR 首先要求
LGWR 将保护这些数据库块的redo 条目刷新输出。DBWR 将任何有修改的块写至磁盘之前,LGWR 必须先刷新
输出与这些块相关的redo 信息。这是有道理的——如果我们要刷新输出表T 中已修改的块,但没有刷新输
出与undo 块关联的redo 条目,倘若系统失败了,此时就会有一个已修改的表T 块,而没有与之相关的redo
信息。在写出这些块之前需要先刷新输出重做日志缓存区,这样就能重做(重做)所有必要的修改,将SGA
放回到现在的状态,从而能发生回滚。
从第二个场景还可以预见到一些情况。这里描述的条件是“如果刷新输出了表T 的块,但没有刷新输
出undo 块的相应redo,而且此时系统失败了“,这个条件开始变得有些复杂。随着增加更多用户、更多
的对象,再加上并发处理等因素,条件还会更复杂。
此时的情况如图9-1 所示。我们生成了一些已修改的表和索引块。这些块有一些与之关联的undo 段
块,这3 类块都会生成redo 来保护自己。如果还记得第4 章中对重做日志缓冲区的讨论,应该知道,它会
在以下情况刷新输出:每3 秒一次;缓冲区1/3 满时或者包含了1MB 的缓冲数据;或者是只要发生提交就
会刷新输出。重做日志缓冲区还有可能会在处理期间的某一点上刷新输出。在这种情况下,其状态如图9-2
所示。
2. UPDATE
UPDATE 所带来的工作与INSERT 大体一样。不过UPDATE 生成的undo 量更大;由于存在更新,所以需
要保存一些“前“映像。系统状态如图9-3 所示。
块缓冲区缓存中会有更多新的undo 段块。为了撤销这个更新,如果必要,已修改的数据库表和索引
块也会放在缓存中。我们还生成了更多的重做日志缓存区条目。下面假设前面的插入语句生成了一些重做
日志,其中有些重做日志已经刷新输出到磁盘上,有些还放在缓存中。
假想场景:系统现在崩溃
启动时,Oracle 会读取重做日志,发现针对这个事务的一些重做日志条目。给定系统的当前状态,
利用重做日志文件中对应插入的redo 条目,并利用仍在缓冲区中对应插入的redo 信息,Oracle 会“前滚”
插入。最后到与图9-1 类似的状态。现在有一些undo 块(用以撤销插入)、已修改的表块(刚插入后的状
态),以及已修改的索引块(刚插入后的状态)。由于系统正在进行崩溃恢复,而且我们的会话还不再连接
( 这是当然),Oracle 发现这个事务从未提交,因此会将其回滚。它取刚刚在缓冲区缓存中前滚得到的undo,
并将这些undo 应用到数据和索引块,使数据和索引块“恢复”为插入发生前的样子。现在一切都回到从前。
磁盘上的块可能会反映前面的INSERT,也可能不反映(这取决于在崩溃前是否已经将块刷新输出)。如果
磁盘上的块确实反映了插入,而实际上现在插入已经被撤销,当从缓冲区缓存刷新输出块时,数据文件就
会反映出插入已撤销。如果磁盘上的块本来就没有反映前面的插入,就不用去管它——这些块以后肯定会
被覆盖。
这个场景涵盖了崩溃恢复的基本细节。系统将其作为一个两步的过程来完成。首先前滚,把系统放到
失败点上,然后回滚尚未提交的所有工作。这个动作会再次同步数据文件。它会重放已经进行的工作,并
撤销尚未完成的所有工作。
假想场景:应用回滚事务
此时,Oracle 会发现这个事务的undo 信息可能在缓存的undo 段块中(基本上是这样),也可能已经
刷新输出到磁盘上(对于非常大的事务,就往往是这种情况)。它会把undo 信息应用到缓冲区缓存中的数
据和索引块上,或者倘若数据和索引块已经不在缓存中,则要从磁盘将数据和索引块读入缓存,再对其应
用undo。这些块会恢复为其原来的行值,并刷新输出到数据文件。
这个场景比系统崩溃更常见。需要指出,有一点很有用:回滚过程中从不涉及重做日志。只有恢复和
归档时会当前重做日志。这对于调优是一个很重要的概念:重做日志是用来写的(而不是用于读)。Oracle
不会在正常的处理中读取重做日志。只要你有足够的设备,使得ARCH 读文件时,LGWR 能写到另一个不同
的设备,那么就不存在重做日志竞争。许多其他的数据库(非Oracle)都把日志文件处理为“事务日志”。
这些数据库没有把redo 和undo 分开。对于这些系统,回滚可能是灾难性的,回滚进程必须读取日志,而
日志写入器正在试图写这个日志。这就向系统中最薄弱的环节引入了竞争。Oracle 的目标是:可以顺序地
写日志,而且在写日志时别人不会读日志。
3. DELETE
同样,DELETE 会生成undo,块将被修改,并把redo 发送到重做日志缓冲区。这与前面没有太大的不
同。实际上,它与UPDATE 如此类似,所以我们不再啰嗦,直接来介绍COMMIT。
4. COMMIT
我们已经看到了多种失败场景和不同的路径,现在终于到COMMIT 了。在此,Oracle 会把重做日志缓
冲区刷新输出到磁盘,系统状态如图9-4 所示。
已修改的块放在缓冲区缓存中;可能有一些块已经刷新输出到磁盘上。重做这个事务所需的全部redo
都安全地存放在磁盘上,现在修改已经是永久的了。如果从数据文件直接读取数据,可能会看到块还是事
务发生前的样子,因为很有可能DBWR 还没有(从缓冲区缓存)写出这些块。这没有关系,如果出现失败,
可以利用重做日志文件来得到最新的块。undo 信息会一直存在,除非undo 段回绕重用这些undo 块。如果
某些对象受到影响,Oracle 会使用这个undo 信息为需要这些对象的会话提供对象的一致读。