在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的错误。
Oracle为了保证用户检索数据的一致性, 通过UNDO记录,当用户检索数据库数据时,Oracle总是使用户只能看到被提交过的数据或特定时间点的数据(select语句时间点),UNDO记录会被存放到回滚段中,假如该数据未提交,用户检索数据时,都是从UNDO记录中取得的.(如下图:)
1. ORACLE检索数据一致性
先打开一个SecureCRT.(第一个session)
先建一个表
- SQL> create table c(a int);
- Table created.
- SQL> alter table c add b number;
- Table altered.
- SQL> desc c
- Name Null? Type
- ----------------------------------------- -------- --------------------------------------------
- A NUMBER(38)
- B NUMBER
表中插入数据并提交
- SQL> insert into c values(1,2);
- 1 row created.
- SQL> insert into c values(3,4);
- 1 row created.
- SQL> select * from c;
- A B
- ---------- -----------------------------
- 1 2
- 3 4
- SQL> commit;
- Commit complete.
再打开一个SecureCRT.(第二个session)
查询
- SQL> select * from c;
- A B
- ---------- --------------------------
- 1 2
- 3 4
第一个session更改表中的数据但不提交
- SQL> update c set b=10 where a=1;
- 1 row updated.
第二个session查询(修改但没有提交检索的是UNDO中的数据)
- SQL> select * from c;
- A B
- ---------- --------------------------
- 1 2
- 3 4
第一个session提交
- SQL> commit;
- Commit complete.
第二个会话查询(可见只有提交后才能检索到数据段的数据)
- SQL> select * from c;
- A B
- ---------- -------------------------
- 1 10
- 3 4
结论:如果用户修改数据但没有提交,其它用户检索的都是UNDO段的数据,这样就保证了数据的一致性
2.回滚数据(事务恢复)
1.当用户updata数据但还没有提交
- SQL> select * from c;
- A B
- ---------- -----------------------------
- 1 10
- 3 4
- SQL> update c set b=2 where a=1;
- SQL> select * from c;
- A B
- ---------- -----------------------------
- 1 2
- 3 4
这时用户突然后悔了,想恢复到原来的状态
- SQL> rollback;
- Rollback complete.
- SQL> commit;
- SQL> select * from c;
- A B
- ---------- -----------------------
- 1 10
- 3 4
可见当用户用命今rollback还能回滚到初始状态.
2.当用户updata数据且已提交
当用户updata数据且已提交后,可以根据SCN记录把数据还源.
先查看原始数据
- SQL> select * from c;
- A B
- ---------- ----------
- 1 10
- 3 4
找到SCN
- SQL> select current_scn from v$database;
- CURRENT_SCN
- -----------
- 693636
现在删除表中的数据并提交
- SQL> delete from c;
- 2 rows deleted.
- SQL> commit;
- Commit complete.
查询(现在表中已没有数据了)
- SQL> select * from c;
- no rows selected
检索特定SCN的数据
- SQL> select * from c as of scn 693636;
- A B
- ---------- ----------
- 1 10
- 3 4
恢复数据
- SQL> insert into c select * from c as of scn 693636;
- 2 rows created.
- SQL> commit;
- Commit complete.
现在再查询
- SQL> select * from c;
- A B
- ---------- ----------------------
- 1 10
- 3 4
可见可以根据SCN恢复到某一检查点的数据,如果把SCN转换成时间,,就可以把数据恢复到某一时间点.