Oracle中undo实现一致性读
toc
在Oracle中undo主要有以下三个作用:提供一致性读,回滚事务,实例恢复。
一致性读的实现
1.什么是一致性读
我们都知道数据库中有脏读,那么与脏读相对应的就是一致性读。举一个简单的例子,这里有一个表,表中有100000行数据,进行一次全表查询需要消耗10分钟。时间是15:00时我开始了查询,那么我将在15:10时得到100000行结果。但是在我开始查询过3分钟时,你把这个表最后一行数据删掉了,那么当查询结束了我得到的结果应该是多少呢?
如果返回了100000行数据,那么发生的是一致性读,如果返回了99999行数据那么发生了脏读。所以说脏读和一致性读是相对的。
这里有一个问题就是,我现在确实只有99999条数据了,但是我开始查询的时间点也就是15:00那个时候查询是有100000行数据的。这个100000才符合当时数据的逻辑关系,返回这个结果才实现了数据库的一致性读。
注意:Oracle数据库是不被允许脏读的,必须提供一致性读,并且这个一致性读还不能影响到用户的DML操作(不能为了一致性读就把表给锁上导致业务不可用)。
2.undo如何实现一致性读
继续上面的例子来说,当我在15:00开始这次查询以后,服务器进程会相应的把15:00的SCN号记录下来假设为1500。那么在15:00这个时刻1500就是最大的SCN,这个SCN也是以后进行衡量的基准。
这里需要引入一个ITL的概念,ITL全称为 Interested Transaction List,是Oracle中数据块的组成部分,用来记录在这个数据块上发生的所有事务,一个ITL可以记录一个事务不论这个事务是否已经提交,一个数据块可以有多个ITL。如果这个事务已经提交了那么这个ITL的位置就可以被反复使用了,因为ITL类似记录,所以,有的时候也叫ITL槽位。ITL槽中会记录对应undo块的地址。
可以说上面记录的1500SCN号在15:00的时候大于所有的数据块上ITL记录的SCN(多个ITL取最大SCN)
执行查询时,服务器进程扫描这个表中的数据块时,会把每个数据块ITL槽中最大的SCN与1500进行比较,如果比1500小则说明这个数据块没有被修改服务器进程直接进行数据读取即可。如果数据块ITL槽中的SCN大于1500那么说明这个数据块在发起查询后被修改了,需要借助undo去获取15:00那个时刻数据块的数据。
根据上面的例子,我是在15:00开始的查询,而数据是在15:03的时候被修改(这里不用考虑有没有提交,因为ITL只要数据块被修改就会有记录,那么这个查询就会去读undo数据块)。我们假设这个被修改的数据块是n号数据块,修改后n号数据块的ITL中记录的SCN是1520,当服务器进程扫描到这个数据块进行SCN比较时发现这个数据块的SCN要大于1500,服务器进程就知道了这个数据块在发起查询后被修改了,于是服务器进程到n号数据块的头部找到1520对应的ITL槽,然后找到对应undo块的位置。将undo块中所存放的n号块修改前的数据取出再结合n号块里的数据行进而构建15:00这个时间点未被修改的数据块,这个被新构建的数据块被称为CR块(Consistant Read)。然后服务器进程扫描这个块,得到15:00一致性的数据,返回正确的100000行数据。
因为我们的修改操作是delete,那么undo中对应的信息就是insert,insert将被删除的数据插入到CR块中,实现一致性读。
3.当事务被提交ITL重用怎样实现一致性读
仍然使用上面的信息作为例子,我们来考虑这样一个场景。当我开始查询后,B在15:03删除了一条记录并提交,在15:05的时候用户C又插入了一条数据,假设B和C所做的修改都发生在一个数据块上了,那么这个一致性读又该如何实现?
前面有提到ITL槽是可以被重用的这也是它为什么被称之为‘槽’的原因。当B删除后提交,那么这个槽就会被重用,删除时这个数据块的ITL记录下SCN为1520,提交后C再插入这个ITL已经变成了1530,这么来看相当于把之前删除的这个事务覆盖了,undo最多只能找到SCN为1520时的数据块数据而1500时的数据找不回来了。事实是这样吗?当然是否定的,这其中的关键就在于Oracle在记录undo数据的时候,不仅仅只记录了数据块改变前的数据也记录了改变前数据块头部的ITL槽的信息。
具体流程如下:
当用户B进行删除时这个时候n号块ITL槽中记录的信息为(undoblock0_scn1500),发起删除后,undo块undoblock1记录此时ITL槽信息(undoblock0_scn1500)并把对应的插入信息记录到undo块中。删除完成提交后,这个n号块的ITL槽信息变成了(undoblock1_scn1520),过了5分钟用户C插入了一条数据,undo块undoblock2记录此时ITL槽信息(undoblock1_scn1520)并把对应的删除信息记录到undo块中。这个时候n号块的ITL槽信息变成了(undoblock2_scn1530)。
执行查询的时候,服务器进程扫描到n号块,发现此时n号块的ITL槽所记录的SCN为1530大于1500就到对应的undoblock2中查看,找到undoblock2发现里面记录的scn为1520仍然大于1500,然后又继续找相应的undoblock1,在undoblock1中发现scn是1500说明此时的数据块符合这次查询的要求,服务器进程就不再查找,结合undoblock2,undoblock1和当前n号块通过undo块中记录的信息构建CR块(将当前n号的数据复制到CR块里,然后在CR块里先回退15:05的事务,也就是在CR块里删除两条记录,然后再回退15:03分的事务,也就是在CR块里插入被删除的记录,从而构建出15:00时的数据)
Oracle在解决一致性问题上就是通过这种层层嵌套的原则查找整个undo链表直到发现ITL槽里的SCN号小于等于发出查询时的那个SCN号为止。
4.ORA-1555
正常来说,当前undo块里记录的SCN号要比上一个undo块里记录的SCN号要小。但是有可能在查找的过程中发现当前undo块里记录的ITL槽的SCN号比上一个undo块里记录的SCN号还要大这是为什么呢?这种情况说明由于事务被提交或回滚,导致当前找到的undo块里的数据已经被其他事务覆盖了,于是我们无法再找出小于等于发出查询时的那个时间点的SCN号。这个时候Oracle就抛出ORA-1555这个经典的错误,“快照过旧”。
抛出这个错误往往是因为Oracle读不到需要的undo数据无法构建一致性的CR块。而造成读不到undo数据的原因可能有很多但是根本原因都是Oracle所需要的undo块被覆盖了。SQL语句执行时间太长,或者UNDO表空间过小,或者事务量过大,或者过于频繁的提交这些原因都可能导致undo被覆盖。
大概有以下几种解决方式:
(1)增加UNDO表空间大小
(2)增加undo_retention时间,默认只有15分钟
(3)优化出错的SQL,减少查询的时间,首选方法
(4)避免频繁的提交