当前台进程发出SELECT或者其他DML语句时,oracle根据SQL语句的执行计划所找到的数据块,会构造一个名为数据块描述(buffer descriptor)的内存结构。该buffer descriptor位于session的PGA中,所包含的内容主要是数据块所在的物理地址、数据块的类型、数据块所属对象的object id等信息。
随后,oracle会把对数据块请求的锁定模式以及所构造出来的buffer descriptor传入专门搜索数据块的函数中。在该函数中,oracle根据buffer descriptor所记录的信息,应用hash算法以后,得到要找的数据块所处的hash bucket,也就是确定该数据块在哪条hash chain上。然后,oracle进入该hash chain,从上面所挂的第一个buffer header开始搜索,一直搜索到最后一个buffer header。
在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上。
在前面,我们已经知道了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作为该参数值。
SQL> alter system set "_db_block_lru_latches"=1 scope=spfile;
SQL> startup force
SQL> show parameter _db_block
NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
_db_block_lru_latches integer 8
我们已经知道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的首端,也就保证了这些数据块可以尽可能的不被替换掉,从而保证了访问的效率。
图三
到了8i以后,oracle引入了一种更加复杂的机制来管理LRU上的数据块。8i以后,LRU和LRUW链表都具有两个子链表,分别叫做辅助链表和主链表。同时还对buffer header增加了一个属性:touch数量,也就是每个buffer header曾经被访问过的次数,来对LRU链表进行管理。oracle每访问一次buffer header,就会将该buffer header上的touch数量增加1,因此,touch数量“近似”的体现了某个内存数据块总共被访问的次数。注意,这只是近似,并不精确。因为touch的增加并没有使用latch来管理并发性。这只是一个大概值,表示趋势的,不用百分百的精确。
还是用上面的这个例子来说明。还是假设buffer cache只能容纳4个数据块,同时只有一个hash chain和一个LRU(确切的说应该是一对LRU主链表和辅助链表)。读入第一个数据块时,该数据块对应的buffer header会挂到LRU辅助链表(注意,这里是辅助链表,而不是主链表)的最末端,同时touch数量为1。读取第二个不同的数据块时,该数据块对应的buffer header会挂到前一个buffer header的后面,从而位于LRU辅助链表的最末端,同样touch为1。假设4个数据块全都用完以后的LRU链表可以用下图四描述。每个buffer header的touch数量都为1。
图四
从上图中我们可以看到辅助LRU链表都挂满了,而主LRU链表还是空的。这个时候,前台发出第五句SQL语句,要求返回指定的数据块。这时,oracle发现buffer cache里已经没有空的内存数据块了,于是从辅助LRU链表的尾部开始扫描,也就是从BH1开始扫描,以查找可以被替代的数据块。扫描的过程中按照下面的逻辑来选择被牺牲的(也就是可以被替代的)数据块:
1)如果被扫描到的buffer header的touch数量小于隐藏参数_db_aging_hot_criteria(该参数缺省为2)的值,则选中该buffer header作为牺牲者,并立即返回该buffer header所含有的数据块的地址。
2)如果当前buffer header的touch数量大于_db_aging_hot_criteria的值,则不会使用该buffer header。但是如果当前的_db_aging_stay_count的值小于_db_aging_hot_criteria的值,则会将当前该buffer header的touch值赋值给_db_aging_stay_count;否则将当前buffer header的touch数量减掉一半。
按照上述的逻辑,这时将选出BH1作为牺牲者(因为BH1的touch数量为1,小于_db_aging_hot_criteria
的值),并将其对应的内存数据块的内容清空,同时将当前第五个数据块的内容拷贝进去。但是这里要注意,这个时候该BH1在LRU链表上的位置并不会发生任何的变化。而不会像8i之前的那样,BH1变成LRU链表的首端。
接下来,前台发来了第六句和第七句SQL,分别要返回与第五句和第四句SQL一样的数据块,也就是要返回当前的BH1和BH4。这个时候,oracle会增加BH1和BH4的touch数量,同时将该BH1和BH4从辅助LRU链表上摘下,转移到主LRU链表的中间位置。可以用下图五描述。
图五
这个时候,如果发来了第八句SQL,要求返回与第三句SQL相同的数据块,也就是当前的BH3,则
这时该BH3会插入主LRU链表上的BH1和BH4中间,注意每次向主LRU列表插入buffer header时都是向中间位置插入。如果发来了第九句SQL要求返回BH2,则我们可以知道,BH2会转移到主LRU链表的中间。这个时候,辅助LRU链表就空了,没有buffer header了。
这时,如果又发来第十句SQL,要求返回一个新的、buffer cache中不存在所需内容的数据块时。oracle会先扫描辅助LRU链表,发现上面没有任何的buffer header时,则必须扫描主LRU链表。从尾部开始扫描,采用前面说到的与扫描辅助LRU链表相同的规则挑选牺牲者。挑出的可以被替代的buffer header将从主LRU链表上摘下,放入辅助LRU链表。
从上面所描述的buffer header在辅助LRU链表和主LRU链表之间交替的过程中,我们可以看出,oracle改进LRU链表的管理方式的目的,就是想千方百计的能够将多次被访问的数据块保留在内存里,同时又要平衡有限的内存资源。这种方式相比较8i之前而言,无疑是进步很多的。在8i之前中,某个数据块可能只会被访问一次,但是就这么一次的访问就将该数据块放到了LRU的首端,从而可能就挤掉了一个LRU上不是那么经常被访问,但是也会多次访问的数据块。而8i以后,将访问一次的数据块和访问一次以上的数据块彻底分开,而且查找可用数据块时,始终都是从辅助LRU链表开始扫描。实际上也就使得越倾向于只访问一次的数据块越快的从内存中清理出去。
从前面我们已经知道SELECT语句读取数据块到buffer cache的过程。那么我们必然会产生另外一个疑问,就是当使用DML等语句修改了buffer cache里的内存数据块以后的过程是怎样的?实际上,为了能够最有效、安全的完成将内存数据块写入数据文件的过程,oracle提供了比读取数据块更为复杂的机制。
我们已经知道LRUW表示脏数据块链表,该链表上的buffer header指向的都是已经从LRU链表上摘下来、其对应的内存数据块里的内容已经被修改、但是还没有被写入数据文件的内存数据块。在这些脏数据块在能够被重用之前,它们必须要被DBWR写入磁盘。从8i以后,LRUW链表同样包含两个子链表:辅助LRUW链表和主LRUW链表。那么LRUW链表是如何产生buffer header的呢?oracle又是如何对其进行管理的呢?
我们还是接着上面图五所示的例子来说明。假设这个时候,前台用户发出DML语句,要求修改BH2所指向的内存数据块。这时,按顺序发生下面的动作:
1) oracle会将BH2从辅助LRU链表上摘下,同时插入主LRU链表的中间,也就是插入BH1和BH4中间,同时增加BH2的touch的数量。
2) 将该BH2的标记设置为钉住(ping)。
3) 更新BH2对应的内存数据块的内容。
4) 更新完以后,取消钉住的标记。
5) 将BH2从主LRU链表转移到主LRUW链表上。
6) 如果这个时候又有进程发出更新BH2所对应的内存数据块的内容,则BH2再次被钉住,更新,取消钉住。
7) DBWR启动以后,在扫描主LRUW链表时会将BH2转移到辅助LRUW链表上。
8) DBWR将辅助LRUW链表上的BH2对应的数据块写入数据文件。
9) 确认成功写入数据文件以后,将BH2从辅助LRUW链表上转移到辅助LRU链表上。
从上面的描述中,我们可以看到,主LRUW链表上包含的buffer header要么是已经更新完了的数据
块,要么是被钉住正在更新的数据块。而当DBWR进程启动以后,它会扫描主LRUW链表,并跳过正在被钉住更新的buffer header,而将已经更新完了的buffer header从主LRUW链表上摘除,并转移到辅助LRUW链表上去。扫描完主LRUW链表,或扫描的buffer header的个数达到一定限度时,DBWR会转到辅助LRUW上,将上面的buffer header所对应的数据块写入数据文件。所以说,对于辅助链表上的buffer header来说,要么是正在等待被写入的;要么就是已经发出写入请求,正在写入而还没写完的。这里要注意的是,buffer header进入LRUW链表,是从尾端进入;而DBWR扫描LRUW链表时,则是从首端开始。
顺带提一句,这里将主LRUW链表和辅助LRUW链表分开,主要就是为了提高DBWR在主LRUW链表上扫描的效率。如果只有主LRUW链表而没有辅助LRUW链表的话,势必造成三种类型buffer header交织在LRUW链表上:1)正在被钉住更新的buffer header;2)已经更新完,而正在等待被写入数据文件的buffer header;3)已经发出写请求,正在写而尚未写完的buffer header。在这种情况下,必然造成DBWR为了找到第二种类型的buffer header而需要扫描不该扫描的第三种类型的buffer header。
图2.JPG
图3.JPG
图4.JPG
图5.JPG
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/9842/viewspace-399667/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/9842/viewspace-399667/