以下摘自:http://book.51cto.com/art/200806/75647.htm  作者:韩思捷


undo表空间管理

对于DML语句来说,只要修改了数据块,Oracle数据库就会将修改前的数据保留下来,保存在undo segment里,而undo segment则保存在undo表空间里。从Oracle 9i起,有两种undo的管理方式:自动Undo管理(Automatic Undo Management,简称AUM)和手工Undo管理(Manual Undo Management,简称MUM)。Oracle 9i之前只能使用MUM,而且在MUM中,undo segment又叫做rollback segment。从Oracle 9i起,Oracle就建议使用AUM,而不应再使用MUM了。因此,我们不讨论MUM以及rollback segment。

读完本章以后,我们能够了解到:


在DML语句的过程中,undo是如何被使用的;

undo的作用是什么,重点在于undo是如何完成一致性读的;

AUM是什么,如何配置AUM。

7.1  DML语句与undo  

当我们发出一条DML(比如update t set col1='A' where col1='B')语句时,其执行过程可大致概括为以下几步。

在shared pool里进行解析,从而生成执行计划。具体解析过程见第5章。

假设根据执行计划,得出col1='B'的记录存放在10号数据文件的54号数据块里。

服务器进程在buffer cache里找一个可用的undo数据块,如果没有发现,则到undo表空间里找一个可用的undo块,并调入buffer cache。假设获得的undo数据块号为24号,位于11号undo数据文件里。

将改变前的值,也就是A放入11号undo数据块。

由于undo数据块发生了变化,于是产生重做记录,假设重做记录号为120。

行号  事务id file# block# row  column  value
120 T1 24 11 10 col1 A

在buffer cache里找到54号数据块。如果没有发现,则从10号数据文件里调入。

将改变后的值,也就是B放入54号数据块。

由于数据块发生了变化,于是产生重做记录,假设重做记录号为121。

行号  事务id file# block# row  column  value
121 T1 10 54 10 col1 B

控制权返回给用户,如果在SQL*Plus里执行DML,则表现为光标返回。

当用户发出commit命令时,触发LGWR进程,将120与121这两个重做记录写入联机日志文件,并将54号数据块和11号undo数据块头部所记录的事务状态标记设置为已提交。然后控制权返回给用户,如果在SQL*Plus里执行DML操作,则表现为光标返回。

这个时候,54号数据块以及11号undo块并不一定被DBWn写入数据文件。只有在脏数据块的数量达到一定程度才会被写入。

事务只要被提交或回滚,那么该事务所使用的undo块就可以被覆盖。对于上面的例子来说,当第 步,用户发出commit命令以后,11号undo块里的数据就可以被其他事务所覆盖。


7.2  undo的作用

在Oracle数据库中,undo主要有三大作用:提供一致性读(Consistent Read)、回滚事务(Rollback Transaction)以及实例恢复(Instance Recovery)。

一致性读是相对于脏读(Dirty Read)而言的。假设某个表T中有10000条记录,获取所有记录需要15分钟时间。当前时间为9点整,某用户A发出一条查询语句:select * from T,该语句在9点15分时执行完毕。当用户A执行该SQL语句到9点10分的时候,另外一个用户B发出了一条delete命令,将T表中的最后一条记录删除并提交了。
那么到9点15分时,A用户将返回多少条记录?

如果返回9999条记录,则说明发生了脏读;如果仍然返回10000条记录,则说明发生了一致性读。很明显,在9点钟那个时间点发出查询语句时,表T中确实有10000条记录,只不过由于I/O的相对较慢,所以才会花15分钟完成所有记录的检索。对于Oracle数据库来说,没有办法实现脏读,必须提供一致性读,并且该一致性读是在没有阻塞用户的DML的前提下实现的。

那么undo数据是如何实现一致性读的呢?还是针对上面的例子。用户A在9点发出查询语句时,服务器进程会将9点那个时间点上的SCN号记录下来,假设该SCN号为SCN9.00。那么9点整的时刻的SCN9.00一定大于等于记录在所有数据块头部的ITL槽中的SCN号(如果有多个ITL槽,则为其中最大的那个SCN号)。

服务器进程在扫描表T的数据块时,会把扫描到的数据块头部的ITL槽中的SCN号与SCN9:00之间进行比较,哪个更大。如果数据块头部的SCN号比SCN9.00要小,则说明该数据块在9点以后没有被更新,可以直接读取其中的数据;否则,如果数据块ITL槽的SCN号比SCN9.00要大,则说明该数据块在9点以后被更新了,该块里的数据已经不是9点那个时间点的数据了,于是要借助undo块。

9点10分,B用户更新了表T的最后一条记录并提交(注意,在这里,提交或者不提交并不是关键,只要用户B更新了表T,用户A就会去读undo数据块)。假设被更新记录属于N号数据块。那么这个时候N号数据块头部的ITL槽的SCN号就被改为SCN9.10。当服务器进程扫描到被更新的数据块(也就是N号块)时,发现其ITL槽中的SCN9.10大于发出查询时的SCN9.00,说明该数据块在9点以后被更新了。于是服务器进程到N号块的头部,找到SCN9.10所在的ITL槽。由于ITL槽中记录了对应的undo块的地址,于是根据该地址找到undo块,将undo块中的被修改前的数据取出,再结合N号块里的数据行,从而构建出9点10分被更新之前的那个时间点的数据块内容,这样的数据块叫做CR块(Consistent Read)。对于delete来说,其undo信息就是insert,也就是说该构建出来的CR块中就插入了被删除的那条记录。随后,服务器进程扫描该CR块,从而返回正确的10000条记录。

让我们继续把问题复杂化。假设在9点10分B用户删除了最后一条记录并提交以后,紧跟着9点11分,C用户在同一个数据块里(也就是N号块)插入了2条记录。这个时候Oracle又是如何实现一致性读的呢(假设表T的initrans为1,也就是只有一个ITL槽)?因为我们已经知道,事务需要使用ITL槽,只要该事务提交或回滚,该ITL槽就能够被重用。换句话说,该ITL槽里记录的已经是SCN9.11,而不是SCN9.10了。这时,ITL槽被覆盖了,Oracle的服务器进程又怎能找回最初的数据呢?

其中的秘密就在于,Oracle在记录undo数据的时候,不仅记录了改变前的数据,还记录了改变前的数据所在的数据块头部的ITL信息。因此,9点10分B用户删除记录时(位于N号块里,并假设该N号块的ITL信息为[Undo_block0 / SCN8.50]),则Oracle会将改变前的数据(也就是insert)放到undo块(假设该undo块地址为Undo_block1)里,同时在该undo块里记录删除前ITL槽的信息(也就是[Undo_block0 / SCN8.50])。删除记录以后,该N号块的ITL信息变为 [Undo_block1 / SCN9.10];到了9点11分,C用户又在N号块里插入了两条记录,则Oracle将插入前的数据(也就是delete两条记录)放到undo块(假设该undo块的地址为Undo_block2)里,并将9点11分时的ITL槽的信息(也就是[Undo_block1 / SCN9.10])也记录到该undo块里。插入两条记录以后,该N号块的ITL槽的信息改为 [Undo_block2 / SCN9.11]。

那么当执行查询的服务器进程扫描到N号块时,发现SCN9.11大于SCN9.00,于是到ITL槽中指定的Undo_block2处找到该undo块。发现该undo块里记录的ITL信息为[Undo_block1 / SCN9.10],其中的SCN9.10仍然大于SCN9.00,于是服务器进程继续根据ITL中记录的Undo_block1,找到该undo块。发现该undo块里记录的ITL信息为[Undo_block0 / SCN8.50],这时ITL里的SCN8.50小于发出查询时的SCN9.00,说明这时undo块包含合适的undo信息,于是服务器进程不再找下去,而是将N号块、Undo_block2以及Undo_block1的数据结合起来,构建CR块。将当前N号的数据复制到CR块里,然后在CR块里先回退9点11分的事务,也就是在CR块里删除两条记录,然后再回退9点10分的事务,也就是在CR块里插入被删除的记录,从而构建出9点钟时的数据。Oracle就是这样,以层层嵌套的方式,查找整个undo块的链表,直到发现ITL槽里的SCN号小于等于发出查询时的那个SCN号为止。正常来说,当前undo块里记录的SCN号要比上一个undo块里记录的SCN号要小。

但是在查找的过程中,可能会发现当前undo块里记录的ITL槽的SCN号比上一个undo块里记录的SCN号还要大。这种情况说明由于事务被提交或回滚,导致当前找到的undo块里的数据已经被其他事务覆盖了,于是我们无法再找出小于等于发出查询时的那个时间点的SCN号,这时Oracle就会抛出一个非常经典的错误——ORA-1555,也就是snapshot too old的错误。

以上的描述可以用图7-1来描述:

undo表空间管理_表空间管理

回滚事务则是在执行DML以后,发出rollback命令撤销DML所作的变化。Oracle利用记录在ITL槽里记录的undo块的地址找到该undo块,然后从中取出变化前的值,并放入数据块中,从而对事务所作的变化进行回滚。


实例恢复则是在SMON进程完成前滚并打开数据库以后发生。SMON进程会去查看undo segment头部(所谓头部就是undo segment里的第一个数据块)记录的事务表(每个事务在使用undo块时,首先要在该undo块所在的undo segment的头部记录一个条目,该条目里记录了该事务相关的信息,其中包括是否提交等),将其中既没有提交也没有回滚,而是在实例崩溃时被异常终止的事务全部回滚。


7.3  配置AUM

要配置AUM,我们先要设置初始化参数undo_management。该参数用来说明undo的管理方式,它有两个取值。


auto:表示采用AUM来管理undo。

manual:表示采用MUM来管理undo,这也就意味着我们要手工创建rollback segment等。

在AUM下,我们只需要创建undo表空间(可以使用前面描述的使用Database Control的图形界面来创建undo表空间,也可以使用SQL命令),并指定初始化参数undo_tablespace,该参数说明数据库当前使用哪个undo表空间。剩下的工作,包括undo segment的创建、扩展、收缩、删除等,都由数据库自动完成,如下所示:

SQL> show parameter undo_management
NAME TYPE VALUE
----------------- ----------- -------------
undo_management string AUTO
SQL> create undo tablespace undonew datafile '/u01/app/
oracle/oradata/ora10g/undonew01. dbf' size 10M autoextend on;
SQL> alter system set undo_tablespace=undonew;


如果我们指定了undo_management为auto,但是在设定undo_tablespace时,指定了某个不存在的undo表空间,则实例启动时报错;如果没有设定undo_tablespace,则系统会查找第一个可用的undo表空间;如果没有找到可用的undo表空间,则使用位于系统表空间里的rollback segment,这样非常不好,增加了系统表空间的压力。

当发生一个DML操作时,服务器进程会选择一个undo segment。AUM采用的是事务绑定undo segment的算法。该算法说明如下。

首先尝试将每个undo segment绑定一个事务,也就是每个undo segment上只被一个事务使用。

如果不能发现完全空闲的(也就是没有与任何事务绑定)undo segment,则系统会尝试将其他脱机的undo segment联机。

如果没有可用的undo segment进行联机,则会尝试创建一个新的undo segment。

如果上面的步骤都没有成功,比如由于没有可用空间了,而不能创建新的undo segment,则事务绑定算法会去尝试找最早被使用的那个undo segment。这种情况下,不同的多个事务才会在一个相同的undo segment里同时运行。

既然存在undo segment的扩张,就存在undo segment的收缩。收缩由SMON完成,在下面的情况下会收缩undo segments:

每隔12个小时会收缩一次,删除那些idle状态的extents;

当前台进程进行DML而需要undo时,发现空间不够用,则会唤醒SMON进行一次收缩。也就是说,将其他undo segment里暂时没被使用的extent拿过来用。

从前面的描述中,我们已经知道,当Oracle在提供一致性读的过程中,会根据ITL槽里记录的undo块的地址,而搜索所有相关联的undo块。只要使用undo块的事务提交或者回滚,则这些undo块就不再被事务所需要;也就是说,其中包含的旧的数据可以被覆盖。这样就会发生在查找改变前的旧值的过程中,找不到足够旧的值,而抛出ORA-1555的错误消息。为此,Oracle为undo的管理提供了另一个参数:undo_retention。该参数以秒为单位,表示当事务提交或回滚以后,该事务所使用的undo块里的数据需要保留多长时间;当保留的时间超过undo_retention所指定的时间以后,该undo块才能够被其他事务覆盖。

当我们使用AUM,并设置了undo_retention以后,undo块就存在四种状态。


Active:表示正在使用该undo的事务还没有提交或回滚。

Inactive:表示该undo上没有活动的事务,该状态的undo可以被其他事务覆盖。

Expired:表示该undo持续inactive的时间超过undo_retention所指定的时间。

Freed:表示该undo块内容是空的,从来没有被使用过。

当活动的事务使用undo segment时,在AUM模式下,事务可以在不同的undo segment之间动态交换undo空间,也就是在不同的undo segment里交换extents。当一个正在执行的事务需要更多的undo空间时,首先会重用当前undo segment里的可用空间;如果当前undo segment里的可用空间(也就是extents)不足时,则会按照下面的步聚获得所需要的extent:


获取undo表空间里可用的、空的extents;

获取其他undo segment里的expired状态的extents;

如果undo表空间里的数据文件启用了自动扩展(autoextend on),则数据文件进行自动扩展;

如果undo表空间里的数据文件没有启用自动扩展,则获取其他undo segment里的INACTIVE状态的extents;

如果以上步骤均无法获得可用空间时,报空间不足的错误消息。

这种动态的方式使得空间的使用最为高效。我们来举例说明,如图7-2所示。

undo表空间管理_undo_02

在图7-2中,假设undo表空间中总共存在5个undo segment(US1、US2、US3、US4和US5)。每个undo segment中包含7个extent,其中每个extent的状态采用A、I、X和F来表示。那么当使用US5的事务还需要额外的可用空间时,首先会使用US5中两个状态为F的extents;如果不够用,则使用US4中的3个状态为F的extents;如果还不够用,则使用US3中的3个状态为X的extents;如果仍然不够用,则使用US1中的3个状态为I的extents;如果再不够用,则报空间不足的错误消息。

从这里可以看出,在AUM里,分配undo块的算法不仅有效,而且会尽可能地将INACTIVE状态的extents保留足够长的时间。在Oracle 10g里,如果undo表空间有足够的可用空间,则Oracle会将undo信息保留的时间与当前运行时间最长的那个查询所需要的时间相同。

默认情况下,Oracle 10g会每隔30秒钟就收集统计信息来自动调整undo retention,收集的信息包括运行时间最长的查询、产生undo的速度等。我们可以通过设置undo_retention参数来改变这种自动的调整。该参数的默认值为900秒。如果我们没有为该参数指定值,或指定了0,则Oracle会自动调整undo retention,并以900秒作为最低值。否则,使用我们设置的该值作为undo保留的时间。


如果设置undo_retention为0,则实例会获取运行时间最长的那个查询所需要的时间,比如为N秒,然后将undo信息保留N秒。当undo表空间的尺寸太小,而不能保留这个最长的时间时,则会尽可能地利用现有的可用空间来让undo保留的时间尽可能长,并不会立刻扩展undo数据文件。除非要覆盖的undo信息是在距今900秒以内发生的,才会去扩展数据文件。


7.4  管理undo表空间

对于undo表空间的管理来说,与一般的表空间区别不大。我们可以对undo表空间使用alter命令,主要用来添加数据文件、重命名数据文件、将数据文件联机或脱机。比如,我们对undo表空间添加一个新的数据文件:

SQL> alter tablespace undonew add datafile '/u01/app/oracle
/oradata/ora10g/undonew02.dbf' size 10M autoextend on;

可以看到,该SQL命令与普通的表空间管理没有任何区别。

数据库中可以同时存在多个undo表空间,但是在一个时间点上,数据库只能使用一个undo表空间。如果我们将undo_tablespace参数设置为另外一个undo表空间的名字,则这叫做undo表空间的切换。

当我们切换undo_tablespace时,如果旧的undo表空间上有事务正在执行,则该旧的undo表空间变成pending offline状态。同时用户的事务可以正常执行,切换操作立刻结束,它并不会等待使用旧的undo表空间的事务结束。发生切换以后,所有新的事务所产生的undo数据不会存放在该旧的undo表空间里,而是会使用新的undo表空间。我们不能使用pending offline状态的undo表空间,该状态下的undo表空间也不能被删除。最终,当旧的undo表空间上的所有的事务都提交以后,旧的undo表空间从pending offline状态变成offline状态,这时我们才可以删除该旧的undo表空间。

在删除undo表空间时,与删除一般的表空间不同。也就是说,如果drop tablespace后面跟的是某个undo表空间的名字,就相当于发出drop tablespace including contents命令。一个undo 表空间只有在当前没有被活动的事务使用的时候才能被删除。如果undo表空间还包含某个未结束的事务,比如某个事务异常中断,但是还没有回滚,这时drop tablespace会报错。但是,drop tablespace命令能够删除那些含有inactive状态的undo块(这些undo块还没有expired)的undo表空间。这样就有可能发生ORA-1555错。因此,我们在切换undo_tablespace以后,应该在等待的时间超过undo_retention的长度以后再删除旧的undo表空间。

我们知道,undo的信息只是尽可能地保留undo_retention所指定的时间。当undo数据文件不能自动扩展时,同时可用的undo块不够用时,Oracle也还会将保留时间小于undo_retention的undo数据覆盖掉。从Oracle 10g开始,我们可以通过为undo表空间设置retention guarantee属性,从而不让这种情况出现。也就是说,当undo数据文件不能自动扩展,并且undo块不够用时,直接报错,而不是覆盖那些inactive而又没有expired的undo块。

retention guarantee属性可以在创建undo表空间时指定,也可以在创建完毕以后设置:

SQL> create undo tablespace undonew1 datafile '/u01/app/
oracle/oradata/ora10g/undonew01. dbf' size 10M autoextend
on maxsize 100M retention guarantee;
SQL> alter tablespace undonew retention guarantee;

若要取消retention guarantee属性,则使用下列命令:

SQL> alter tablespace undonew retention noguarantee;

在我们计算undo表空间大小时,可以借助视图v$undostat。Oracle每隔10分钟更新该视图,将这10分钟里产生的undo块的个数记录在该视图里。Oracle在v$undostat里保留最近7天的数据,也就是1008行记录。

SQL> select end_time,undoblks from v$undostat;
END_TIME UNDOBLKS
------------------- ---------
2007-10-04 19:37:32 1
2007-10-04 19:34:21 5
2007-10-04 19:24:21 8
2007-10-04 19:14:21 2
2007-10-04 19:04:21 51

因此,通过该视图,我们可以计算undo表空间应该设置多大,Oracle提供了如下公式:

UndoSpace = Undo Retention * Undo Per Second + overhead(24 undo blocks)

其中,undo retentio就是系统中运行时间最长的那个查询所花费的时间,我们把该时间设置到初始化参数undo_retention上去。而Undo Per Second则可以通过查询v$undostat来计算:

Select blks/((end-begin)*3600) as "Undo Per Second" from 
(Select min(begin_time) begin,max(end_time) end,sum(undoblks) blks from v$undostat);

当然,在判断undo表空间应该设置多大时,借助Database Control所提供的Undo Advisory会更加简单,如图7-3所示。其中,重点部分为①部分。这里,横轴为undo retention,以秒为单位,纵轴为undo表空间大小尺寸,以MB为单位。蓝色线条上的点表示当设定横轴上的undo retention时,需要多大的undo表空间(从对应的纵轴可以获得)。


undo表空间管理_tablespace_03