本章提要
-----------------------------------------------
redo, undo 定义
redo, undo 如何工作
如何访问 redo, undo
提交和回滚
-----------------------------------------------
redo: 用来重做(前滚)
undo: 用来回滚(后滚)
redo: 重做日志文件, 数据库的事务日志, online redo, archived log两类(都是磁盘文件)
如果数据库所在的机房掉电, oracle会使用在线重做日志将系统恰好恢复到掉电之前的那个提交点.
如果磁盘出现问题, oracle会使用归档重做日志以及在线重做日志将该驱动器上的数据备份恢复到适当时间点.
另外,如果你"不小心"删除了一个表,或者删除了某些重要信息,然后提交了这个操作,可以恢复数据的一个备份,
并使用在线和归档重做日志文件把它恢复到这个"意外"发生前的时间点.
归档重做日志文件实际上就是已经填满的"旧"在线重做日志文件的副本.
每个oracle数据库都至少有两个在线重做日志组, 每个组中至少有一个成员(重做日志文件), 这些在线重做日志组以
循环的方式使用, oracle会先写组1中的日志文件,等写到组1中的文件的最后时, 将切换日志文件组2, 开始写这个组
内的文件,等到把日志文件组2写满时, 会再次切换回日志文件组1(假设之后两个日志文件组)
undo: 从概念上说, undo 刚好与 redo 相对. 你对数据库执行修改时, 数据库会生成 undo 信息, 以便回到更以前的状态.
redo 用于在失败是重放事务(即恢复事务), undo 用于在取消一条语句, undo 在数据库内部存储在一个特殊的段中,
undo 段, undo 的作用很大, 并非只是将数据库物理的恢复到执行语句之前的样子. 例如:
假如我们的事务执行了一个insert语句,这条语句导致分配一个新区段(也就是说,导致表空间增大)通过这个insert,我们
将得到一个新的块,格式化这个块以便使用, 并在其中放上一些数据, 此时, 可能出现另外某个事务, 它也向这个块中
插入数据, 如果要回滚我们的事务, 显然不能取消这个块的格式化和空间分配, 因此, oracle回滚时,它实际上会做与
先前逻辑上相反的工作(注意是逻辑上), 对于每个insert, oracle会完成一个delete, 对于每个delete, oracle会执行
一个insert. 对于每个update, 会执行反update.
怎样才能看到undo生成的具体情况?
1) 创建一个空表
2) 对它做全表扫描, 观察该表执行I/O数量
3) 在表中填入许多行(但没有提交)
4) 回滚这个工作,并撤销
5) 再次进行全表扫描, 观察锁执行的I/O数量
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
create table t as select * from all_objects where 1 = 0; set autotrace traceonly statistics select * from t; set autotrace off insert into t select * from all_objects; rollback; set autotrace traceonly statistics select * from t; -- 前一个查询的 consistent gets 为 30 -- 后一个查询的 consistent gets 为 1112 -- 前面 insert 导致将一些块增加到高水位, 全表扫描是扫描到高水位
redo 和 undo 如何协作
尽管undo信息存储在undo表空间的undo段中, 但也会收到redo保护,
换句话说, 会把 undo 数据当成是表数据或索引数据一样, 对 undo 的修改会生成一些redo,
这些 redo 会记入日志, 为什么会这样? 稍后系统崩溃, 你就会明白为什么要这样.
场景1 insert-update-delete(redo, undo 如何协作)
insert into (x,y) values(1,1);
update t set x = x+1 where x = 1;
delete from t where x = 2;
我们会沿着不同路径完成这个事务, 从而得到以下问题的答案.
1) 如果系统在处理这些语句的不同时间点上失败, 会发生什么情况?
2) 如果在某个时间点上 rollback, 会发生什么情况?
3) 如果成功commit, 会发生什么情况?
insert 语句
对于insert语句, redo 和 undo 都会生成, 所生成的 undo信息足以使 "insert 消失"(这条语句的逻辑消失), 所生成的
redo信息足以让这个插入"再次发生".
插入后, 这里(块缓冲区)缓存了已经修改的 undo 块, 索引块, 表数据块, 这些块得到重做日志缓冲区中相应条目的"保护",
redo 信息自动写到重做日志缓冲区
假设, 系统现在崩溃, 重启就好像这个事务根本没发生过一样. 没有将任何已经修改的内容刷新输出到磁盘, 也没有任何
redo 刷新输出到磁盘, 我们不需要这些 undo 或 redo 信息来实现实例失败恢复.
假设, 块缓冲区已满, dbwr 必须留出空间, 要把修改的块从缓存刷新输出, 即便是这些块没有提交,但是oracle没有办法,
因为空间不足,其他事务无法将块拿到缓存处理, 所以这个时候, dbwr首先要求lgwr将保护这些数据库块的redo
条目刷新输出(即便没有commit), 此时情况如图9-1, 我们生成了一些已修改的表和索引块, 这些块有一些与之关联的undo段块,
这3类块都会生成 redo 来保护自己, 这样就来到了状态如果9-2, 即lgwr刷新输出内容到磁盘.(经过以上可知, 磁盘上的
文件并非是"干净的", 有的时候, 由于内存没有空间,需要将一些文件存储在磁盘上, 尽管这些数据块并没有被提交),
也就是说, 我们修改了缓冲区高速缓存中没有提交的修改, 并将磁盘的那些未提交的修改重做, 这种正常的情况经常发生,
所以才会出现,当我们查询时, 真正的数据块里存储的是没有提交的值, 我们要反过来查询undo中的内容, 在这个时刻,实际上
在undo中的数据才是"正确的", 除非修改当前数据块的session,提交了,那么我们再查询的时候,才是直接得到数据块中的值.
注意, 我们一直没有显示提交 commit, 而lgwr自己本身会有很多限制, 比如每3秒, 缓冲区1/3满 等
(commit 会将该事务写到磁盘的redo log file中)
假如, 此时(就是磁盘上有很多没有commit的数据, 同样online redo log file中也同样存在)系统崩溃了, 重启oracle后,
1) 首先, 那些没有写入 online redo log file 的事务(即原来在内存中)会被忽略, 因为它们做的内容被没有被保存, 类似
上边的第一种假设(系统崩溃)
2) 然后根据 redo online log file 进行 redo, 即向前滚(重做), 因为 redo中有很多"脏事务"(已经commit的, 当然没得说,
是干净的, 另外还有uncommit的但是, 结果已经保存在磁盘上或者还没来得急保存在存盘上,全部都重做)
3) oracle 可以发现在 online redo log file 中哪些事务没有被提交, 那么就要回滚这些没有被提交的事务, 它取得刚刚(恢复
重做redo过程中产生的那些undo, 并将这些undo应用到数据和索引快, 使数据和索引快"恢复"为发生事务之前的样子)
有一点很有用: 回滚过程中从不涉及重做日志, 只有恢复(redo)和归档(archivelog)时才会读取重做日志, 这对于调优是一个
很重要的概念: 重做日志是用来写的(而不是用于读), oracle不会在正常的处理中读取重做日志.
提交和回滚处理
执行 commit 时所做的工作:
1) 为事务生成一个 SCN, SCN用于保证事务的顺序, 并支持失败恢复, SCN还用于保证数据库的读一致性和检查点. 可以把
SCN看作是一个钟摆, 每次有人commit时, SCN就会增加1.
2) LGWR将所有余下的缓存重做日志条目写至磁盘, 并且把SCN记录到在线重做日志文件中. 如果出现了这一步, 即已经
提交, 事务条目会从 $transaction中"删除".
3) v$lock中记录着我们会话持有的锁, 这些锁都将被释放.
执行 rollback 做的工作:
1) 撤销已做的所有修改, 从undo段读回数据, 然后逆向执行所有之前的操作, 并将undo条目标记为已用.
2) 会话持有的所有锁都将撤销
可见, rollback的开销要比commit大.
分析 redo
作为一个开发人员, 应该能够测量你的操作生成了多少redo, 这往往很重要, 生成的redo越多, 你的操作话费的时间就
越长, 整个系统也会越慢, 你不光影响你自己的会话, 还会影响每一个会话, redo管理是数据库中一个串行点, 任何
oracle实例都只有一个LGWR, 最终所有事务都会归于LGWR, 要求这个进程管理它们的redo, 并commit事务.
传统路径 insert(每天正常用的) 与 直接路径 insert(向数据库中加载大量数据时) 比较
直接路径加载: insert /*+ append */ into t select * from big_table; -- 会发现很少的 redo 信息(noarchivelog模式下)
不能关掉重做日志生成的程序, 但是可以通过设置, 减少日志生成.
不生成重做日志的方法nologging(1在sql中, 2在索引上)
<1> 在sql中设置nologging, 这里生成的重做日志只是"少的多", 而并非完全没有
实验 nologging
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
select log_mode from v$database; -- result archivelog drop table t; variable redo number exec :redo := get_stat_val('redo size'); create table t as select * from all_objects; exec dbms_output.put_line((get_stat_val('redo size')-:redo) || 'bytes of redo generated...'); -- test 2 drop table t; variable redo number exec :redo := get_stat_val('redo size'); create table t nologging as select * from all_objects; exec dbms_output.put_line((get_stat_val('redo size')-:redo) || 'bytes of redo generated...'); -- 生成的日志文件少了很多, 但是不是没有 -- 一定要非常谨慎使用 nologging, 因为nologging的内容在恢复时会遇到极大的困难.
关于 nologging, 需要注意一下几点:
1) 事实上, 还是会生成一些redo, 这些redo作用是保护数据字典.
2) nologging不能避免后续操作生成redo, 比如创建表时使用了nologging, 但是之后的insert, 肯定是要产生redo
3) 在archivelog模式的数据库上执行nologging后,必须尽快为受影响的数据文件建立一个新的基准备份,从而避免丢失
<2>在索引上设置nologging
例如把一个索引或表默认采用 nologging模式, 这说明, 以后重建这个索引不会生成日志
实验
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
create index t_idx on t(object_name); variable redo number exec :redo := get_stat_val('redo size'); alter index t_idx rebuild; exec dbms_output.put_line((get_stat_val('redo size')-:redo) || 'bytes of redo generated...'); -- test 2 nologging alter index t_idx nologging; exec :redo := get_stat_val('redo size'); alter index t_idx rebuild; exec dbms_output.put_line((get_stat_val('redo size')-:redo) || 'bytes of redo generated...');
nologging小结
1) 索引的创建和alter(重建)
2) 表的批量 insert (通过 /*+append*/提示使用直接路径insert, 表数据部生成redo, 但是表索引修改会生成redo
3) lob操作
4) 通过 create table as select 创建表
5) 各种 alter table操作, 比如 move 和 split.
为什么不能分配一个新日志
如果数据库试图重用一个在线重做日志文件, 但是发现做不到, 就会把这样一条消息写到服务器上的alert.log中, 如果
DBWR还没有完成重做日志所保护数据的检查点(checkpoint), 或者 arch 还没有把重做日志文件复制到归档目标, 就会
发生这种情况, 对用户来说, 这时数据库会hang在那, 完成了检查点或归档日志之后, 又恢复正常. 一般初始的日志文件
会太小, 而不能满足大工作量, 解决这个问题:
1) 让dbwr快一些(把内存中的数据文件快一点写入磁盘,这样就能快一点完成checkpoint)
2) 增加更多重做日志文件, 更多组
3) 重新创建更大的日志文件
4) 让检查点发生的更频繁, 更连续
块清除 删除所修改数据库块上的与"锁定"有关的信息.(只需了解)
数据锁实际上是数据的属性, 存储在块首部, 这就带来一个副作用, 下一次访问这个块时, 可能必须清理这个块, 换句话说,
要将这些事务信息删除. 这个动作会生成 redo, 并导致块变脏(原本并不脏, 因为数据本身没有修改), 这说明一个简单的
select也可能生成redo, 而且可能导致完成下一个检查点时将大量的块写至磁盘, 不过,在大多数情况下,这是不会发生的.
最理想的是, commit可以完成块清除, 这样后面的 select(读)就不必再清理了, 只要我们修改的块数没有超过缓存中总块数
的10%, 而且块仍在缓存中并且是可用的, oracle就会再提交时清理这些块, 否则, 将不清理.
测试清理 与 工作原理
首先, 将 DB_CACHE_SIZE 设置为16M, 这足以放下2048个8K块(我的数据库的块大小是8k), 然后创建一个表, 其中每行刚好
能在一个块中放下(我不会在每个块里放两行). 接下来在这个表中插入 10000行, 并提交, 因为 10000 远大于 2048块, 所以
在提交时数据不可能清理所有的脏块. 甚至它们中的大多数都不会缓存在缓冲区高速缓存中. 我要测量到此为止生成的redo量,
然后运行一个select, 它会访问每个块, 最后测量这个select生成的redo.
-- 为了保证这个例子的可重复性, 需要禁用SGA自动内存管理, 如果启用了SGA自动内存管理, 有可能数据库会增加缓冲区缓存
大小, 而这样, commit同时会进行块清除, 那我们的实验就没有意义了.
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
-- 一行就占用1块 create table t ( id number primary key, x char(2000), y char(2000), z char(2000) ) / -- 避免硬解析的干扰 exec dbms_stats.set_table_stats( user, 'T', numrows=>10000, numblks=>10000 ); -- 运行这个代码块失败, 不用担心, 本来就是要失败的 -- 运行这个代码块的目的, 就是为了不用担心硬解析的副作用 declare l_rec t%rowtype; begin for i in 1 .. 10000 loop select * into l_rec from t where id=i; end loop; end; / insert into t select rownum, 'x', 'y', 'z' from all_objects where rownum <= 10000; commit; -- 测试 select 会生成 redo variable redo number exec :redo := get_stat_val( 'redo size' ); declare l_rec t%rowtype; begin for i in 1 .. 10000 loop select * into l_rec from t where id=i; end loop; end; / exec dbms_output.put_line( (get_stat_val('redo size')-:redo) || ' bytes of redo generated...'); -- 再次运行select, 可以看到一个redo都没有 variable redo number exec :redo := get_stat_val( 'redo size' ); declare l_rec t%rowtype; begin for i in 1 .. 10000 loop select * into l_rec from t where id=i; end loop; end; / exec dbms_output.put_line( (get_stat_val('redo size')-:redo) || ' bytes of redo generated...');
如果执行了一个大的 insert, update, delete, 这种块清除行为影响最大, 你会注意到, 在此之后, 第一个"接触"块的查询会
生成少量的redo, 并把块弄脏,如果DBWR已经将块刷新输出或者实例已经关闭, 可能就会因为这个查询而导致重写这些块, 并完成
清理缓冲区缓存, 如果oracle不对块完成这种延迟清除, 那么commit的处理就会与事务本身一样长. 提交必须重新访问每一个块,
可能还要从磁盘将块再次读入, 例如, 假设你更新(update)了大量数据,然后提交(commit), 现在对这些数据
运行一个查询来验证结果, 看上去查询生成了大量写I/O和redo, 如果你不知道存在块清除, 你就无法解释.
在一个OLTP系统中, 可能从来不会看到这种情况发生, 因为OLTP系统的特点是事务都很短小, 只会影响为数不多的一些块.
日志竞争
提交太过频繁, 可能导致, 例如在循环内使用提交.还有其他原因, 如:
redo 放在一个慢速磁盘上, redo 与 其他频繁操作的磁盘放在一个设备上. redo 采用了慢速技术, 例如 raid-5
临时表的 redo/undo
临时表不会为它们的块生成redo, 因此, 对临时表的操作是不可恢复的, 不过, 临时表会生成undo, 而且这个undo会记入日志, 因此,
临时表也会生成一些redo(redo保护undo), 为什么需要生成undo? 这是因为你能回滚到事务中的一个savepoint, 可以擦除对临时表的
后50个Insert, 而只留下前50个, 临时表上的DML活动, 得出以下一般结论
1) insert会生成很少甚至不生成undo/redo活动
2) delete在临时表上生成的redo与在永久表上生成的redo一样多
3) 临时表的update会生成永久表update一半的redo.(也有例外情况)
有了以上的总结, 你可能避免使用delete临时表, 而使用 trancate 临时表(当然要记住, truncate是ddl操作)或者只是让临时表在
COMMIT之后或会话终止时自动置空. 应该尽量避免update临时表.
分析 undo
什么操作会生成最多和最少的 undo ?
如果存在索引(或者实际上就是索引组织表), 这将显著地影响生成undo的量, 因为索引是一种复杂的数据结构, 可能会生成相当多的
undo 信息. 一般来讲, insert生成的undo最少, 因为oracle为此锁需记录的只是要"删除"的一个rowid(可见undo确实是反逻辑操作,
并非是简单的物理逆过程), update一般排名第二(大多数情况下), 对于update, 只需记录修改的字节, 你可能只update了整个数据行
的很少的一部分, 这种情况最常见. delete 生成的undo最多, 对于delete, oracle必须把整行的映像记录到undo段中(逆操作是insert).
在 redo 生成方面, delete生成的redo最多, 而且对于临时表而言的DML操作, 只会把delete的undo记入日志, 这实际上也表明了delete
会生成最多的undo. 另外还要考虑索引的开销(这个让然,因为要维护索引的结构)
ora-01555: snapshot too old 错误
导致这个错误的一个原因: 提交的太过频繁, 那么那还哪些原因, 在这仔细讨论一下.
1) undo 段太小, 不足以在系统上执行工作
2) 你的程序跨commit获取(实际上这是前一点的一个变体)
3) 块清除
解决办法:
1) 适当设置参数 undo_retention
2) 使用手动undo管理时加大或增加更多的回滚段
3) 减少查询运行时间(调优), 这是一个好办法
例如:
可见, commit以后, 在 undo 段中的内容, 就准许被覆盖了. 问题的主要根源, 就是查询语句运行了太长时间了. 也就是说,
如果设置 undo 段太小, 使得很有可能在执行查询期间重用这些undo段, 而且查询要访问被修改的数据, 那就很有可能不断地遭遇
ora-01555错误, 在这种情况下, 必须把 undo_retention参数设置的高一些, 或者重新设置 undo段的大小.
自动管理undo, 采用这种办法, 通过 undo_retention参数告诉 oracle 要把undo保留多长时间, 剩下的工作, oracle自动完成.
演示 ora-01555 错误
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
create undo tablespace undo_small datafile '/tmp/undo.dbf' size 2m autoextend off / alter system set undo_tablespace = undo_small; create table t as select * from all_objects order by dbms_random.random; alter table t add constraint t_pk primary key(object_id) / exec dbms_stats.gather_table_stats( user, 'T', cascade=> true ); begin for x in ( select rowid rid from t ) loop update t set object_name = lower(object_name) where rowid = x.rid; commit; -- 提交以后, 对应的undo信息就可以被覆盖, 如果undo信息被覆盖了 -- 那么, 下边查询就无法查询到正确的值, 就会返回 ora-01555错误 end loop; end; / -- 在这段修改运行的同时, 我们会在另一个session运行查询 -- 另一个session, 在查询中使用 first_rows提示, 使之使用前面创建的索引 -- 从而通过索引(按object_id排序)来读出表中的行, 由于数据是随机的插入 -- 表中的, 我们可能随机的查询表中的块 declare cursor c is select /*+ first_rows */ object_name from t order by object_id; l_object_name t.object_name%type; l_rowcnt number := 0; begin open c; loop fetch c into l_object_name; exit when c%notfound; dbms_lock.sleep( 0.01 ); l_rowcnt := l_rowcnt+1; end loop; close c; exception when others then dbms_output.put_line( 'rows fetched = ' || l_rowcnt ); raise; end; /
要修正以上的错误, 我们要做到以下两点:
1) undo_retention要设置足够长时间, 以保证这个读进程完成, 这样数据库就能扩大undo表空间来保留足够的undo(自动管理undo)
2) undo表空间可以增长, 或手动分配更多的磁盘空间.
alter database
datafile '/tmp/undo.dbf'
autoextend on
next 1m
maxsize 2048m;
这时候,当我们执行上边的语句时, 会看到现在的undo使用量:
select bytes/1024/1024
from dba_data_files
where tablespace_name = 'UNDO_SMALL'; -- result 20, (20m)
延迟的块清除
块清除是导致 ora-01555错误的一个原因, 尽管很难完全杜绝, 不过好在并不多见, 因为很少见, 这里就不分析了.