在《ORA-1555错误解决一例》一文中,当时尝试模拟UNDO段头事务表被覆盖的情况下出现ORA-01555错误,没有成功。实际上没有成功的原因是事务数虽然多,但是事务过小,使UNDO块没有被覆盖完,这样通过回滚事务表仍然能够得到事务表以前的数据。本文进一步讨论一些有关延迟块清除和一致性读方面的内容(但不会涉及到延迟块清除和一致性读的具体概念和过程,只是一些有趣的东西)。
先来看看一个数据块中ITL的转储:
Block header dump: 0x0200410a Object id on Block? Y seg/obj: 0x5f07 csc: 0xb08.1303a672 itc: 3 flg: - typ: 1 - DATA fsl: 0 fnx: 0x0 ver: 0x01 Itl Xid Uba Flag Lck Scn/Fsc 0x01 0x0001.025.0000472d 0x00000000.0000.00 C--- 0 scn 0x0b08.12f461da 0x02 0x0007.015.00004ba0 0x0080d9a0.16f7.39 C-U- 0 scn 0x0b08.12fb5cae 0x03 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
我们看看ITL中的第2个条目,其Flag为"C-U-",C表示commit(提交),U表示Upper bound(上限),这里”U“表明ITL中记录的事务的提交SCN并不是精确的,只是表明事务提交的精确SCN是在ITL中记录的SCN之前,对于第2条ITL来说,其事务提交SCN小于或等于”0x0b08.12fb5cae“。那么这里的问题是:Upper bound是在什么情况下出现的?如果一个SQL语句对该块进行一致性读时,发现ITL中的Upper bound的SCN比一致性读需要的SCN大,这时会发生什么?要回答这些问题,先来看下面的一系列测试过程:
1. 在会话1中建测试表t1,将插入500行数据,每个块只有1行数据,一共500个块,然后再创建一个较大的测试表t2,插入1000行数据:
SQL> @mysid SID ---------- 160 SQL> create table t1 ( id number, small_vc varchar2(20),padding varchar2(1000)) pctfree 90 pctused 10; 表已创建。 SQL> insert /*+ append */ into t1 2 select rownum,rownum || lpad('0',10,'0'),lpad('0',1000,'0') 3 from dba_objects 4 where rownum< =500; 已创建500行。 SQL> SQL> commit; 提交完成。 SQL> create table t2 (id number,vc varchar2(20),padding varchar2(1000)) pctfree 90 pctused 10; 表已创建。 SQL> insert /*+ append */ into t2 2 select rownum,lpad(rownum,20,'0'),lpad(rownum,1000,'0') 3 from dba_objects 4 where rownum<=1000; 已创建1000行。 SQL> commit; 提交完成。 SQL> select dbms_rowid.rowid_relative_fno(rowid),dbms_rowid.rowid_block_number(rowid) 2 from t1 3 where rownum<=5; DBMS_ROWID.ROWID_RELATIVE_FNO(ROWID) DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID) ------------------------------------ ------------------------------------ 8 16650 8 16651 8 16652 8 16653 8 16654
2. 在会话1中更新测试表T1中的所有行,并获取事务ID,然后再dump1个数据块和事务对应的UNDO段头块:
SQL> update t1 set padding=lower(padding); 已更新500行。 SQL> select xidusn,xidslot,xidsqn,to_char(start_scnw,'xxxxxxxx') start_scnw, 2 to_char(start_scnb,'xxxxxxxx') start_scnb, 3 start_scnb+start_scnw*power(2,32) start_scn from v$transaction; XIDUSN XIDSLOT XIDSQN START_SCN START_SCN START_SCN ---------- ---------- ---------- --------- --------- ----------------- 7 21 19360 b08 12f461db 12129305649627 SQL> select file_id,block_id from dba_rollback_segs where segment_name='_SYSSMU7$'; FILE_ID BLOCK_ID ---------- ---------- 2 105 SQL> alter system dump datafile 8 block 16650; 系统已更改。 SQL> alter system dump datafile 2 block 105; 系统已更改。
事务使用的事务表在回滚段_SYSSMU7$上,即第7个回滚段。事务表中的条目为21,即事务表中的第21条记录。
数据块dump出来的结果是(去掉了对本文话题无关紧要的内容,以后也是如此):
*** 2012-05-26 11:14:38.439 Start dump data blocks tsn: 8 file#: 8 minblk 16650 maxblk 16650 buffer tsn: 8 rdba: 0x0200410a (8/16650) scn: 0x0b08.12f461f5 seq: 0x01 flg: 0x00 tail: 0x61f50601 frmt: 0x02 chkval: 0x0000 type: 0x06=trans data Hex dump of block: st=0, typ_found=1 Block header dump: 0x0200410a Object id on Block? Y seg/obj: 0x5f07 csc: 0xb08.12f461ce itc: 3 flg: - typ: 1 - DATA fsl: 0 fnx: 0x0 ver: 0x01 Itl Xid Uba Flag Lck Scn/Fsc 0x01 0x0001.025.0000472d 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000 0x02 0x0007.015.00004ba0 0x0080d9a0.16f7.39 ---- 1 fsc 0x0000.00000000 0x03 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
可以看到ITL中的第2条正是当前活动事务在这个块上所使用的ITL。Xid为“0x0007.015.00004ba0”,转换成10进制正是“7.21.19360”,这跟之前查询出来的事务ID是一致的。ITL中此时的flag为"----",正是活动事务的标志。由于块中只有1行数据,因此Lck为1,即该事务在这个块中锁住的行数为1行。
下面再来看看此时UNDO段头块的转储结果:
*** 2012-05-26 11:14:51.052 Start dump data blocks tsn: 1 file#: 2 minblk 105 maxblk 105 buffer tsn: 1 rdba: 0x00800069 (2/105) scn: 0x0b08.12f46417 seq: 0x01 flg: 0x00 tail: 0x64172601 frmt: 0x02 chkval: 0x0000 type: 0x26=KTU SMU HEADER BLOCK Hex dump of block: st=0, typ_found=1 ............................. TRN CTL:: seq: 0x16f7 chd: 0x0008 ctl: 0x002c inc: 0x00000000 nfb: 0x0000 mgc: 0x8201 xts: 0x0068 flg: 0x0001 opt: 2147483646 (0x7ffffffe) uba: 0x0080d9a0.16f7.39 scn: 0x0b08.12eca506 Version: 0x01 TRN TBL:: index state cflags wrap# uel scn dba parentxid nub stmt_num cmt ------------------------------------------------------------------------------------------------ 0x00 9 0x00 0x4bc4 0x0027 0x0b08.12f45e35 0x0080d9a0 0x0000.000.00000000 0x00000002 0x00000000 1338001717 ................................ 0x14 9 0x00 0x4c1d 0x001d 0x0b08.12f37a3b 0x0080b31e 0x0000.000.00000000 0x0000009a 0x00000000 1338001345 0x15 10 0x80 0x4ba0 0x0031 0x0b08.12f461db 0x0080d9e8 0x0000.000.00000000 0x00000049 0x00000000 0 0x16 9 0x00 0x4bc8 0x002e 0x0b08.12ed360f 0x0080da1e 0x0000.000.00000000 0x0000009a 0x00000000 1338001284 ............. 0x2c 9 0x00 0x4c00 0xffff 0x0b08.12f45f06 0x0080d9a0 0x0000.000.00000000 0x00000001 0x00000000 1338001844 0x2d 9 0x00 0x4b9d 0x001a 0x0b08.12eec008 0x00000000 0x0000.000.00000000 0x00000000 0x00000000 1338001299 0x2e 9 0x00 0x4ba0 0x0004 0x0b08.12ed994e 0x0080e51e 0x0000.000.00000000 0x0000009a 0x00000000 1338001287 0x2f 9 0x00 0x4bbf 0x000c 0x0b08.12f00a3e 0x0080549e 0x0000.000.00000000 0x0000009a 0x00000000 1338001311
可以看到index为0x15(即10进制21)的行,其wrap#为0x4ba0,与事务的xid相符,同时其state为10,表示事务是活动的,而事务表项上的scn值也是跟之前从v$transaction中查询出来的值是完全一致的。
3. 在会话1中将所有的block刷出内存,然后提交,这样T1表中的所有块上的事务都不会被清除,再将UNDO段头dump出来:
SQL> alter system flush buffer_cache; 系统已更改。 SQL> commit; 提交完成。 SQL> alter system dump datafile 2 block 105; 系统已更改。
看看UNDO段头的转储结果:
*** 2012-05-26 11:20:33.746 Start dump data blocks tsn: 1 file#: 2 minblk 105 maxblk 105 buffer tsn: 1 rdba: 0x00800069 (2/105) scn: 0x0b08.12f4649c seq: 0x01 flg: 0x00 tail: 0x649c2601 frmt: 0x02 chkval: 0x0000 type: 0x26=KTU SMU HEADER BLOCK Hex dump of block: st=0, typ_found=1 TRN CTL:: seq: 0x16f7 chd: 0x0008 ctl: 0x0015 inc: 0x00000000 nfb: 0x0001 mgc: 0x8201 xts: 0x0068 flg: 0x0001 opt: 2147483646 (0x7ffffffe) uba: 0x0080d9a0.16f7.39 scn: 0x0b08.12eca506 Version: 0x01 index state cflags wrap# uel scn dba parentxid nub stmt_num cmt ------------------------------------------------------------------------------------------------ .................. 0x15 9 0x00 0x4ba0 0xffff 0x0b08.12f4649c 0x0080d9e8 0x0000.000.00000000 0x00000049 0x00000000 1338002416 ..................
从上面的内容可以看到,state已经变为9,表示事务已经提交,提交SCN为0x0b08.12f4649c,而UNDO段头中的“TRN CTL”中的“ctl: 0x0015”表示这一条事务表记录将会是该事务表中SCN最大的一条,即会最后被选择重用。
4. 现在我们在会话2中,使用只读事务来模拟开始长时间的查询,其查询时间点就是只读事务设置时当前的时间点:
SQL> @mysid SID ---------- 152 SQL> col before_bigtx_scn for 9999999999999999 SQL> select sys.dbms_flashback.get_system_change_number before_bigtx_scn from dual; BEFORE_BIGTX_SCN ----------------- 12129305650318 SQL> set transaction read only; 事务处理集。
5. 然后在会话3里面,发起960次事务,但是每个事务都很少:
SQL> @mysid SID ---------- 153 SQL> begin 2 for i in 1..960 loop 3 update t2 set vc = lpad(i,20,'0'); 4 commit; 5 end loop; 6 end; 7 / PL/SQL 过程已成功完成。
由于我测试的数据库,UNDO表空间中的UNDO段有10个(同时全部都是处于online状态),每个UNDO段头上的事务表有48条记录(在10g中8K大小的块其固定为48条,随着版本和块大小的不同,事务表的大小有变化)。所以960次事务,事务表中的每条记录平均被重用2次,而由于事务较小,UNDO表空间足够大,那么UNDO块不会被覆盖完,即之前会话1上对T1表所做的事务产生的UNDO是不会被覆盖的。
6. 在会话1中,然后转储之前T1表的事务使用的UNDO段头块,得到的内容如下:
*** 2012-05-26 11:27:41.134 Start dump data blocks tsn: 1 file#: 2 minblk 105 maxblk 105 buffer tsn: 1 rdba: 0x00800069 (2/105) scn: 0x0b08.13039a62 seq: 0x01 flg: 0x00 tail: 0x9a622601 frmt: 0x02 chkval: 0x0000 type: 0x26=KTU SMU HEADER BLOCK Hex dump of block: st=0, typ_found=1 ..... TRN CTL:: seq: 0x1708 chd: 0x001f ctl: 0x001e inc: 0x00000000 nfb: 0x0001 mgc: 0x8201 xts: 0x0068 flg: 0x0001 opt: 2147483646 (0x7ffffffe) uba: 0x00809aa1.1708.11 scn: 0x0b08.12fb5cae Version: 0x01 TRN TBL:: index state cflags wrap# uel scn dba parentxid nub stmt_num cmt ------------------------------------------------------------------------------------------------ 0x15 9 0x00 0x4ba2 0x0008 0x0b08.1301d042 0x00809a0d 0x0000.000.00000000 0x00000014 0x00000000 1338002842
可以看到,这里seq从原来的0x16bf变成了0x1708,即事务表中的记录用了73次,看来在各个UNDO段头之间的事务表的使用并不完全是平均的。
7. 在会话1中,查询T1表中的1条记录,然后再转储T1表上第1行数据的数据块:
SQL> select * from t1 where rownum<=1; ...省略查询表返回的1行结果.... SQL> alter system dump datafile 8 block 16650; 系统已更改。 --转储结果-- *** 2012-05-26 11:29:09.250 Start dump data blocks tsn: 8 file#: 8 minblk 16650 maxblk 16650 buffer tsn: 8 rdba: 0x0200410a (8/16650) scn: 0x0b08.1303a672 seq: 0x01 flg: 0x00 tail: 0xa6720601 frmt: 0x02 chkval: 0x0000 type: 0x06=trans data Block header dump: 0x0200410a Object id on Block? Y seg/obj: 0x5f07 csc: 0xb08.1303a672 itc: 3 flg: - typ: 1 - DATA fsl: 0 fnx: 0x0 ver: 0x01 Itl Xid Uba Flag Lck Scn/Fsc 0x01 0x0001.025.0000472d 0x00000000.0000.00 C--- 0 scn 0x0b08.12f461da 0x02 0x0007.015.00004ba0 0x0080d9a0.16f7.39 C-U- 0 scn 0x0b08.12fb5cae 0x03 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
由于T1表之前的事务在块上的信息并没有清除,因此毫无疑问应该会进行延迟块清除,从上面ITL的数据来看,第2条,也就是之前的事务所使用的那条ITL,Flag已经是"C-U-“,同时Lck为0,表示事务已提交。那么事务提交的SCN是多少呢,这里的SCN是0x0b08.12fb5cae,由于Flag中有"U",表示这个SCN只是一个上限值,这里记录的SCN并不是精确的事务提交SCN,只是表明事务的SCN会比小于等于现在记录的这个SCN值。
那么这个值是怎么来的?这正是之前提出的其中一个问题。可以确定的是,这个块的延迟清除是由会话1发起的,现在描述一下这个过程:会话1查询T1表,很显然这个时候T1表的事务早已经提交,所以我们知道它不是需要回滚表块上的数据的;但是程序就是程序,它只能按照逻辑来进行操作,它检查块的ITL列表时,发现有一个活动事务(由于之前的事务没有清除),所以它通过ITL记录的xid去检查事务是否已经提交,它从UNDO段头事务表的第0x15条记录发现,该记录的wrap#是0x4ba2,并不是事务的seq号0x4ba0,因此这条记录已经被重用,那么可以确认这条记录已经提交,需要进行事务延迟块清除;那么事务的提交SCN是多少呢?由于事记录表中的记录已经被重用,所以不能直接得到;接下来会话会从UNDO段头的"TRN CTL::"部分得到一个SCN值:0x0b08.12fb5cae,这个SCN是事务表中最近被重用的事务表记录之前的事务的提交SCN,由于事务表记录重用是按照提交SCN从小到大的顺序重用的,所以可以说这个SCN比事务表中目前所有记录的事务的提交SCN都要小,但它又是这个事务表中曾经被重用过的事务记录的提交SCN中的最大值,由于我们要查看的事务记录已经被重用过,所以很显然该事务的提交SCN会小于或等于"TRN CTL"记录的SCN。基于这个原理,会话1知道,ITL中需要清除的事务小于等于UNDO段头中TRN CTL记录的SCN,而这个SCN值已经比它的一致性读SCN要小,所以会话1足够判断不需要回滚该事务就能得到一致性读结果,所以这个时候,它就直接将TRN CTL中的SCN作为ITL中的事务提交SCN的上限值,并将Flag设置为"C-U-"。这里会话1不需要回滚事务表就已经足够,它不会做多余的工作来得到精确的事务提交SCN,只需要能判断事务提交时间小于一致性读的SCN就可以了。
8. 在会话2中,执行下面的SQL:
SQL> select * from t1 where rownum<=1;
由于会话2在会话3的大量事务之前就设置了一致性读的起点(通过设置事务为只读模式),很显然,这个时间点比上述第7个操作步骤中会话1的查询时间要早得多,也在上面提到的上限SCN 0x0b08.12fb5cae要小。那么此时会发生什么?
9.在会话1中再一次转储T1表第1行数据所在的数据块,得到的内容如下:
Block header dump: 0x0200410a Object id on Block? Y seg/obj: 0x5f07 csc: 0xb08.1303a702 itc: 3 flg: - typ: 1 - DATA fsl: 0 fnx: 0x0 ver: 0x01 Itl Xid Uba Flag Lck Scn/Fsc 0x01 0x0001.025.0000472d 0x00000000.0000.00 C--- 0 scn 0x0b08.12f461da 0x02 0x0007.015.00004ba0 0x0080d9a0.16f7.39 C--- 0 scn 0x0b08.12f4649c 0x03 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
从上面的结果可以看到,SCN也变成了 0x0b08.12f4649c,这正是T1表上之前的事务提交的准确的SCN,事务Flag从原来的”C-U-“变成了“C---”也说明了现在的SCN是精确的事务提交SCN。这说明了什么?这说明当会话2在读到这个块时,虽然发现事务已经提交,但是其记录的SCN是个上限值,并且大于其一致性读时间点,所以它还需要进一步来查找该事务更接近精确的提交SCN。这个时候就会要回滚事务表,实际上,由于在T1表的事务提交之后而在会话2的查询时间点之前这段时间内,在这个UNDO段中没有事务产生,所以会话2在回滚事务表到最后,就刚好发现了事务的精确提交SCN。那么,这里得到一个重要结论是:如果会话发现ITL中的事务有比现在记录的上限SCN有更精确或更小的提交SCN时,会用新找到的SCN替换到ITL中现在记录的SCN。也同样是一个修改块的操作,也可以认为是一次“块清除”。下面的统计数据是会话2执行SELECT语句的统计数据:
Name Value -------------------------------------------------------------------------------- STAT...physical read IO requests 1 STAT...data blocks consistent reads - undo records applied 1 STAT...transaction tables consistent read rollbacks 1 STAT...immediate (CR) block cleanout applications 1 STAT...commit txn count during cleanout 1 STAT...redo entries 1 STAT...cleanout - number of ktugct calls 1 STAT...db block changes 1 STAT...table scans (short tables) 1 STAT...enqueue requests 1 STAT...enqueue releases 1 STAT...physical read total IO requests 1 STAT...cursor authentications 1 STAT...session cursor cache hits 1 STAT...table scan blocks gotten 1 STAT...table scan rows gotten 1 STAT...physical reads 1 STAT...physical reads cache 1 STAT...cleanouts and rollbacks - consistent read gets 1 STAT...CR blocks created 2 STAT...shared hash latch upgrades - no wait 2 STAT...execute count 2 STAT...parse count (total) 2 STAT...opened cursors cumulative 2 STAT...SQL*Net roundtrips to/from client 3 STAT...calls to kcmgas 3 STAT...calls to get snapshot scn: kcmgss 3 STAT...free buffer requested 3 STAT...user calls 4 STAT...DB time 8 STAT...transaction tables consistent reads - undo records ap 60 STAT...consistent changes 61 STAT...consistent gets - examination 61 STAT...consistent gets from cache 66 STAT...consistent gets 66 STAT...session logical reads 66 STAT...redo size 116 STAT...bytes received via SQL*Net from client 571 STAT...bytes sent via SQL*Net to client 1,777 STAT...physical read total bytes 8,192 STAT...physical read bytes 8,192
immediate (CR) block cleanout applications值为1表示有1次逻辑读产生的块清除,还有"transaction tables consistent reads - undo records application"表示产生了事务表的回滚。
接下来,我们再做一个测试,具体的测试数据不再列出,测试表仍然是T1和T2,步骤如下:
- 会话1更新表T1,将buffer cache中的块全部刷出内存,然后再提交。
- 会话1发起960次小事务更新表T2。
- 会话2执行set transaction read only。
- 会话1发起960次小事务更新表T2。
- 会话3执行set transaction read only。
- 会话1发起960次小事务更新表T2。
- 会话2查询表T1。
- 会话3查询表T2。
上面步骤的第7步完成后,数据块的ITL如下:
Block header dump: 0x0200410a Object id on Block? Y seg/obj: 0x5f0b csc: 0xb08.1327da77 itc: 3 flg: - typ: 1 - DATA fsl: 0 fnx: 0x0 ver: 0x01 Itl Xid Uba Flag Lck Scn/Fsc 0x01 0x0007.028.00004bca 0x00000000.0000.00 C-U- 0 scn 0x0b08.1326548d 0x02 0x0003.02c.00004c00 0x008097ab.159c.03 C-U- 0 scn 0x0b08.1326501e 0x03 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
第8步完成后,数据块的ITL如下:
Block header dump: 0x0200410a Object id on Block? Y seg/obj: 0x5f0b csc: 0xb08.1327da89 itc: 3 flg: - typ: 1 - DATA fsl: 0 fnx: 0x0 ver: 0x01 Itl Xid Uba Flag Lck Scn/Fsc 0x01 0x0007.028.00004bca 0x00000000.0000.00 C-U- 0 scn 0x0b08.1324ce8c 0x02 0x0003.02c.00004c00 0x008097ab.159c.03 C-U- 0 scn 0x0b08.1324d090 0x03 0x0000.000.00000000 0x00000000.0000.00 ---- 0 fsc 0x0000.00000000
上面两次列出的ITL中的第2个条目是我们要关注的,可以看到,会话2查询T1表之后,Flag为”C-U-“,记录的SCN为0x0b08.1326501e,会话3查询T1表之后,Flag仍然为”C-U-“,SCN发生了变化,为0x0b08.1324d090,这个SCN值更小(实际上第1个ITL条目也在变化),但仍然不是精确的提交SCN。
上面的测试这也证明了本文得到的两个结论:
- 在一致性读时进行块清除,并不需要得到事务的精确提交SCN(尽管它通过回滚事务表可能会得到精确的提交SCN),只需要它发现事务的提交SCN比自己的一致性读的时间点小就可以了。
- 如果在一致性读时发现了更小的(更接近精确提交的)SCN值或精确的提交SCN值,它还会做一次块清除,修改ITL,以记录更小或更精确的SCN。
很认真的看了一遍,非常好!
[回复]
太详细了,great!
[回复]
熊老,我执行alter system flush buffer_cache;后发现转储出来的undo slot还是10的状态并且数据block的itl也未显示提交状态,除非进行checkpoint,11.2.0.1。
[回复]
认真看了,很不错,果然是高手,有深度。
[回复]
我的理解是,做延迟块清除的时候,只是标记一下,所以itl是C-U,如果需要构造CR块,就会得到准确的SCN,前提是undo事务表还能回滚。
[回复]
看了好几遍,很好,但是还有个疑问,会话2是怎么找到精确的scn的,用redo恢复undo,然后回滚undo一直回滚到事务提交的时候的scn?
[回复]
11g那个undo guarantee不知道能不能避免both undo block and undo block slot overwritten的情况
[回复]
老熊 回复:
4月 20th, 2013 at 11:49 下午
可以解决这个问题。但是undo表空间的空间大小会是个问题,如果空间不够,事务不能进行引起的可用性问题可是比较严重。
[回复]