一致性读是相对于脏读而言的,假如:某个表T有10000条记录,获取所有记录需要15分钟。当前时间为9点整,某用户A发出select * from T,该语句在9点15分时执行完毕。当用户A执行到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点之后没有被更新,可以直接读取其中的数据;否则,如果数据块头部的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删除了表T最后一条记录并提交,紧跟着9点11分,C用户在同一个数据块里(N号数据块)插入2条记录。这个时候oarcle又是如何实现一致性读的呢(假设表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用户又在该数据块插入了两条记录,则oracle将插入前的数据(即delete两条记录)放到undo块(假设undo块的地址为UNDO_BLOCK2)里,并将9点11分的ITL槽的信息,也就是(UNDO_BLOCK1/SCN9.10)记录到undo块里。插入两条记录以后,N号数据块的ITL槽的信息变为[UNDO_BLOCK2/SCN9.11]。
那么当执行查询的服务器进程扫描到N号块时,发现其ITL槽中的SCN号SCN9.11比SCN9.00大,于是到其ITL槽中指定的UNDO_BLOCK2处找到该undo块。发现该UNDO块里记录的ITL信息为[UNDO_BLOCK1/SCN9.10],其中SCN9.10仍然比SCN9.00大,于是服务器进程根据ITL中记录的UNDO_BLOCK1,找到该undo块。发现该undo块里记录的是[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块,然后从中找到该undo块,然后从中取出变化前的值,并放入数据块中,从而对事务所做的变化进行回滚。
实例恢复则是在SMON进程完成前滚并打开数据块以后发生。SMON进程会去查看undo segment头部(所谓头部就是undo segment里的第一个数据块)记录的事务表(每个事务在使用undo块时,首先在该undo块所在的undo segment的头部目录一个条目,该条目里记录了该事务相关的信息,其中包括是否提交等),将其中既没有提交页没有回滚,而是在实例崩溃时被异常终止的事务全部回滚。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/24496749/viewspace-1045308/ ,如需转载,请注明出处,否则将追究法律责任。