在hash chain上搜索的逻辑如下:
1) 比较buffer header上所记录的数据块的地址,如果不符合,则跳过该buffer header。
2) 跳过状态为CR的buffer header。
3) 如果遇到状态为READING的buffer header,则等待,一直等到该buffer header的状态改变以后再比较所记录的数据块的地址是否符合。
4) 如果发现数据块地址符合的buffer header,则查看该buffer header是否位于正在使用的列表上,如果是,则判断已存在的锁定模式与当前所要求的锁定模式是否兼容,如果是,则返回该buffer header所记录的数据块地址,并将当前进程号放入该buffer header所处的正在使用的列表上。
5) 如果发现锁定模式不兼容,则根据找到的buffer header所指向的数据块的内容,构建一个新的、内容一样的、状态为XCURRENT的复制数据块,并且构造一个状态为CR的buffer header,同时该buffer header指向所新建立的复制数据块。然后,返回该复制数据块的地址,并将当前进程号放入该buffer header所处的正在使用的列表上。
6) 如果比较完整个hash chain以后还没发现所要找的buffer header,则从磁盘上读取数据文件。并将读取到的数据块所对应的buffer header挂到hash chain上。
3.2 LRU和LRUW链表结构及其管理机制
3.2.1 LRU和LRUW链表结构概述
在前面,我们已经知道了oracle是如何在hash chain中搜索要找的数据块所对应的buffer header的过程,我们也知道如果在hash chain上没有找到所要的buffer header时,oracle会发出I/O调用,到磁盘上的数据文件中获取数据块,并将该数据块的内容拷贝一份到buffer cache中的内存数据块里(顺带提一句,内存数据块通常叫做buffer,而数据文件里的数据块通常叫做block,二者是一个意思)。这个时候,假如buffer cache是空的,比较好办,直接拿一个空的内存数据块来用即可。但是如果buffer cache中的内存数据块全都被用掉了,没有空的内存数据块了,怎么办?应该重新使用哪一个内存数据块?当然我们可以一个一个的比较内存数据块与其对应在数据文件中的数据块的内容是否一致,如果一致则可以将该数据块拿来,将其内容清空,然后拷贝上当前数据块的内容;如果不一致,则跳过,再找下一个。毫无疑问,这种方式效率低下。为了高效的管理buffer cache中的内存数据块,oracle引入了LRU和LRUW等链表等结构。
在buffer cache中,最耳熟能详的链表可能就是LRU链表了。在前面描述buffer cache结构的图上,也可以看到有两个链表:LRU和LRUW。在介绍LRU和LRUW前,先说明几个概念。
1) 脏数据块(dirty buffer):buffer cache中的内存数据块的内容与数据文件中的数据块的内容不一致。
2) 可用数据块(free buffer):buffer cache中的内存数据块为空或者其内容与数据文件中的一致。注意,可用数据块不一定是空的。
3) 钉住的数据块(ping buffer):当前正在更新的内存数据块。
4) 数据库写进程(DBWR):这是一个很底层的数据库后台进程。既然是后台进程,就表示该进程是不能被用户调用的。由oracle内置的一些事件根据需要启动该进程,该进程用来将脏数据块写入磁盘上的数据文件。
LRU表示Least Recently Used,也就是指最近最少使用的buffer header链表。LRU链表串连起来的buffer header都指向可用数据块。而LRUW则表示Least Recently Used Write,也叫做dirty list,也就是脏数据块链表,LRUW串起来的都是修改过但是还没有写入数据文件的内存数据块所对应的buffer header。某个buffer header要么挂在LRU上,要么挂在LRUW上,不能同时挂在这两个链表上。
随着硬件技术的发展,电脑的内存越来越大。buffer cache也是越来越大,只用一条LRU和一条LRUW来管理buffer header已经不够用了。同时oracle还引入了多个DBWR后台进程来帮助将buffer cache中的脏数据块写入数据文件,显然,多个DBWR后台进程都去扫描相同的LRUW链表会引起争用。为此oracle引入了working set的概念。每个working set都具有它自己的一组LRU和LRUW链表。每个working set都由一个名为“cache buffers lru chain”的latch(也叫做lru latch)来管理,所以从这个意义上说,每一个lru latch就是一个working set。而每个被加载到buffer cache的buffer header都以轮询的方式挂到working set上去。也就是说,当buffer cache加载一个新的数据块时,其对应的buffer header会去找一个可用的lru latch,如果没有找到,则再找下一个lru latch,直到找到为止。如果轮询完所有的lru latch也没能找到可用的lru latch,该进程只有等待latch free等待事件,同时出现在v$session_wait中,并增加“latch misses”。如果启用了多个DBWR后台进程的话,每个DBWR进程都会对应一个不同的working set,而且每个DBWR只会处理分配给它的working set,不会处理其他的working set。
我们已经知道一个lru latch就是一个working set,那么working set的数量也就是lru latch的数量。而lru latch的数量是由一个隐藏参数:_db_block_lru_latches决定的。该参数缺省值为DBWR进程的数量×8。
该参数最小必须为8,如果强行设置比8小的数值,oracle将忽略你设置的值,而使用8作为该参数值。
1 SQL > alter system set "_db_block_lru_latches" = 1 scope = spfile; 2 SQL > startup force 3 SQL > show parameter _db_block 4 NAME TYPE VALUE 5 -- ---------------------------------- ----------- ------------------------------ 6 _db_block_lru_latches integer 8
3.2.2 深入LRU链表
我们已经知道LRU链表是用来查找可以重用的内存数据块的,那么oracle是怎么使用LRU链表的呢?这里需要分为8i之前和8i以后两种情况。
在8i之前,我们举一个例子。假设buffer cache只能容纳4个数据块,同时只有一个hash chain和一个LRU。当数据库刚刚启动,buffer cache是空的。这时前台进程发出SELECT语句获取数据块时,oracle找一个空的内存数据块,并将其对应的buffer header挂到hash chain上。同时,oracle还会把该buffer header挂到LRU的最尾端。随后前台进程又发出SELECT语句,这时所找到的buffer header在LRU上会挂到前一个buffer header的后面,也就是说第二次SELECT语句所找到的buffer header现在变成了LRU的最尾端了。假设发出4句SELECT以后找到了4个buffer header,从而用完了所有的buffer cache空间。这个时候的LRU可以用下图二来表示。
这个时候,发来了第五句SELECT语句。这时的buffer cache里已经没有空的内存数据块了。但是既然需要容纳下第五个数据块,就必然需要找一个可以被替换(后面会看到类似牺牲、重用的字样,它们和替换都是一个意思)的内存数据块。这个内存数据块会到LRU上去找。按照oracle设定的最近最少使用的原则,位于LRU最尾端的BH1将成为牺牲者,oracle会把该BH1对应的内存数据块的内容清空,并将当前第五句SQL所获得的数据块的内容拷贝进去。这个时候,BH1就成了LRU的首端,而BH2则成为了LRU的尾端。如下图三所示。在这种方式下,经常被访问的数据块可以一直靠近LRU的首端,也就保证了这些数据块可以尽可能的不被替换掉,从而保证了访问的效率。