在oracle数据库中,undo主要有三大作用:提供一致性读(consistent read)、回滚事务(rollback transaction)以及实例恢复(ins
在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号)。
注: itl(interested transaction list)是oracle数据块内部的一个组成部分,用来记录该块所有发生的事务,一个itl可以看作是一个记录,在一个时间,可以记录一个事务(包括提交或者未提交事务)。当然,如果这个事务已经提交,那么这个itl的位置就可以被反复使用了,因为itl类似记录,所以,有的时候也叫itl槽位。
服务器进程在扫描表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的错误。
以上的描述可以用图来描述:
回滚事务则是在执行dml以后,发出rollback命令撤销dml所作的变化。oracle利用记录在itl槽里记录的undo 块的地址找到该undo块,然后从中取出变化前的值,并放入数据块中,从而对事务所作的变化进行回滚。
实例恢复则是在smon进程完成前滚并打开数据库以后发生。smon进程会去查看undo segment头部(所谓头部就是undo segment里的第一个数据块)记录的事务表(每个事务在使用undo块时,首先要在该undo块所在的undo segment的头部记录一个条目,该条目里记录了该事务相关的信息,其中包括是否提交等),将其中既没有提交也没有回滚,而是在实例崩溃时被异常终止的事务全部回滚。