Buffer Cache 是 Oracle 中最重要的内存池之一,本文将对其进行深入讲解。
若要更好地理解本文,需要对 Oracle 体系结构中的内存结构有基本的了解。如果还不知道Buffer Cache的作用、SGA各个部分的基础知识,可先行阅读文章《Oracle 体系结构详解》。
在正式介绍 Buffer Cache 内容前,先介绍两个名词,按照 IT 界的惯常叫法,对于一个块,在磁盘中叫 Block(块);在内存中通常称为 Buffer 。本文将沿用这个习惯叫法。
在Buffer Cache中最重要、最难理解的部分就是它的各种链表,包括HASH链表、LRU链表、检查点队列链表等。因此本文将以链表为主线,挖掘Buffer Cache的工作原理。
HASH链表
小问答:
假设进程要访问 5 号数据文件中的第 1234 号块,Oracle 如何知道这个块是否在 BufferCache 中呢?如果在的话,它的具体地址是多少呢?
答:使用HASH算法。
1)HASH 链表与逻辑读
在 Oracle 中,几乎所有在内存中搜索数据的算法都采用 HASH 算法。
HASH 算法中有一个重要的概念:Bucket。Buffer Cache 中的 HASH Bucket 数量,由_db_block_hash_buckets 参数设置。
所有的 HASH Bucket 结构如图 1 所示。
图 1 HASH 表结构
当进程要读取某一个数据块时,比如,要读取 5 号文件中的第 1234 号块,它将根据文件号、块号来计算 HASH 值。假设此处计算出的 HASH 值为 X,那么进程将根据此值,直接定位到 Bucket X ,如图 2 所示。
图 2 HASH 表定位
定位到这个 Bucket 后,就可以读取它里面的内容。它里面的内容如图 3 所示。
图 3 HASH 表和 Cache Buffers Cache 链表
简单地说,在每个 Bucket 中,都只保存一个指向 Cache Buffers Cache 链表(简称 CBC 链表)的链表头。
先要简单说一下什么是链表。
比如有 3 个人,张三、李四和王五,他们分别住在不同的地方。假设张三的住址是 “中山路123号” ,李四的住址是 “延安路456号” ,王五的住址是 “长安街789号” 。
找一张纸,写上李四的住址 “延安路456号” ,放在张三家中。这张纸,就是指针,它指向李四。这张纸放在张三家中,可以说张三指向李四。同理,将王五的地址放在李四家中,即 “李四指向王五” 。
这样一来,张三指向李四、李四指向王五。这就是单向链表。
在数据结构中,张三、李四、王五被统一称为链表的Node(节点),本文后面也会使用这个统一的称呼:Node。
除单向链表外,当然还有双向链表。张三指向李四,李四指向张三;李四指向王五,王五指向李四,这就是双向链表。Oracle中所有的链表都是双向链表。
张三、李四、王五一旦形成链表,以后再找这 3 个人就方便了,不需要记住他们 3 个人的地址,只需记住一个人的地址就可以了。将张三的地址 “中山路123号” ,写到一张纸上,由于张三是链表中开头第一个人,因此这张纸就叫 “指向链表头的指针” 。Oracle HASH 表的 Bucket 中存放的是什么呢,就是 “指向链表头的指针” 。
假设张三、李四、王五他们几个都是记录 Buffer Cache 中 Buffer 的位置、状态等信息的,他们被称为 Buffer Header(简称为BH)。
Buffer Cache 中每一个 Buffer 都有一个 Buffer Header,但是碰巧张三、李四、王五他们的 HASH 值一样(HASH冲突),因此,他们几个人被组织成了链表,这个链表称为 CBC 链表。Buffer Cache 的 HASH 表的 Bucket中,存放的就是 CBC 链表的链表头。
所有的 HASH 算法,都无可避免 HASH 冲突的问题。解决 HASH 冲突问题的办法,就是在每个 HASH Bucket 后面建立一个链表。
现在既然已经找到 Bucket X 了,下一步当然就是读取它里面存放的 CBC 链表头,再接下来则是搜索链表了。进程将逐个比较每个 BH 中记录的文件号、块号,直到找到需要的为止。在图 3 中, Bucket X 中第二个 BH 就是目标了。
接下来,再看一张图,如图 4 所示。
图 4 逻辑读过程
可以看到,在 BH 中有一项信息很重要,即 BA(Buffer Address)。它是 5 号文件第 1234 号块在 Buffer Cache 中的地址。进程根据这个地址,直接访问 Buffer 就可以了。
通过这几张图,将进程在 Buffer Cache 中搜索 Buffer 的过程描述如下。
- 进程根据要访问块的文件号、块号,计算 HASH 值。
- 根据 HASH 值找到 HASH Bucket 。
- 搜索 Bucket 后的链表,查找哪个 BH 是目标 BH 。
- 找到目标 BH,从中取出 Buffer 的 BA 。
- 按 BA 访问 Buffer 。
这就是 Oracle 逻辑读的过程。如果搜索 Bucket 后的BH链表,没有找到包含目标文件号、块号的 BH,那就证明 uffer Cache 中不包含目标块,就只能物理读了。
2)Cache Buffers Chain Latch与Buffer Pin锁
SGA 中是公共内存,哪怕要访问公共内存中的一个字节,都需要有某种锁机制保护。Oracle 采用的锁机制就是 Latch 和 Mutex 。
在以上逻辑读的过程中,搜索 Bucket 后的链表,还有访问 BH 中的 BA,都需要 Latch 的保护。这个 Latch 就是 Cache Buffers Chain Latch(简称CBC Latch)。
为了让大家对 CBC Latch 有更形象的理解,这里用一幅图来表示,如图 5 所示。
图 5 CBC Latch示意图
在图 5 中,Oracle 在链表前加了一把锁,如果想访问链表,必须先申请获得这把锁。这个锁就是 CBC Latch 。
它不单保护对链表的访问,当在链表中找到目标 BH 时,有时还要对 BH 进行修改,修改的目的是为了加锁,此处的锁可以称为 Buffer Pin 锁。在修改完 BH 中 Buffer Pin 锁的状态后,CBC Latch 就可以释放了。
之后,进程将在 Buffer Pin 锁的保护下访问 Buffer 。
总结一下,获得 CBC Latch 后,进程要完成两个工作:
- 搜索链表,查找目标 BH。
- 修改 BH中Buffer Pin 锁的状态。
Buffer Pin 锁有多种模式,最常见的有共享(用字母S表示)、独占(用字母X表示)两种模式。在没有加锁的时候,Buffer Pin 锁的值将是 0 。
说明:Buffer Pin 锁还有一种模式也较为常见,就是为 “当前读” 加锁,后文会有介绍。
如果只是逻辑读,进程会将 Buffer Pin 锁的状态设置为 S 模式;如果是 DML 操作,要修改 Buffer,进程将把 Buffer Pin 锁设置为X模式。
Buffer Pin 锁的详情后面再描述,先回到 CBC Latch 上来。事实上,图 5 并不是十分准确,从该图中很容易得出,每个 HASH Bucket 都有一个专门的 CBC Latch 保护,但其实不是。事实上,一个 CBC Latch 要保护好几个 Bucket ,如图 6 所示。
图 6 一个 CBC Latch 保护几个 Bucket
在图 6 中,Bucket X、Bucket Y、Bucket Z 三个 Bucket 都和同一个 CBC Latch 相关。也就是用同一个 CBC Latch 保护了 3 个 Bucket 。
但要注意的是,每个 Bucket 后还是有各自的链表(图 6 未画出 Bucket Y 和 Bucket Z 后面的链表 ),只是这些链表都用同一个 CBCLatch 保护而已 。
Oracle 这样做是为了节约内存。因为每个 Latch 都会占用一部分内存,如果每个 Bucket 都对应一个 CBC Latch,那么就会多占用一些内存。
在图 6 中,在 5 号文件 1234 号块的 BH 中,添加了一行:Buffer Pin 。它其实只是 BH 中的几个字节,但它同时也是一个锁。只有将它成功地修改成 S 或 X,才能进一步访问 Buffer 。
再强调一下,开始访问 Buffer 的时候,CBC Latch 就释放了,Buffer 的访问将在 Buffer Pin 锁的保护下完成。
访问完 Buffer 后,当然还要释放 Buffer Pin 锁,也就是说,还要修改 BH 中的 BufferPin 锁。修改这个锁时,同样还需要 CBC Latch 的保护。事实上,CBC Latch 一共也就这么两个作用,即保护链表和保护 BH 。
CBC Latch 也有两种持有模式:共享和独占。但要注意的是,不同于 Buffer Pin 锁用读、写形式来决定锁的模式,就算为了 “读” 而持有 CBC Latch,有时会是独占模式,而有时则会是共享模式。CBC Latch 的持有模式取决以下 4 个要素:
- 对象类型(唯一索引、非唯一索引等)。
- 块类型(根块、叶块或表块等)。
- 操作(读、修改)。
- 访问路径(Accees Path)。
除有唯一索引外,在大多数情况下,无论是读还是写,访问表块都将以独占模式获得 CBC Latch 。另外,索引的根块、枝块只要不修改,都是以共享模式获得 CBC Latch 的。
注意,前面说过,CBC Latch 的目的有两个:保护链表和保护 BH 。其实 Oracle 所有锁的根本原理就是,只要不修改,都以共享模式获得。如果涉及修改,当然就要以独占模式获得了。
对于 Buffer,就算只是读其中的数据,仍然要修改 BH 中 Buffer Pin 锁的状态。而修改 BH,自然就要以独占方式请求 CBC Latch 了。具体步骤如下:
步骤1:以独占方式获得 CBC Latch,如图 7 所示。
图 7 逻辑读时独占CBC Latch
步骤2:在独占 CBC Latch 的保护下,修改 BH 中的 Buffer Pin 锁,将锁状态改为 S(原来是0),如图 8 所示。
图 8 修改锁状态
步骤3:BH 中的 Buffer Pin 锁状态修改完毕,释放独占的 CBC Latch,如图 9 所示。
图 9 释放 CBC Latch
步骤4:在共享 Buffer Pin 锁的保护下,到 BH 中的 BA 找到 Buffer 的地址,读取 Buffer 中的数据,如图 10 所示。
图 10 读取Buffer中的数据
这是一般逻辑读的流程。
但是对于索引的根块、枝块等这些块的查询频度,要远高于叶块和表块。这点应该很容易理解,如果你的索引有 100 万个块,但根块只有一个。无论访问这 100 万个块中的哪一个,都要先访问根块。可以想象,根块、枝块的查询频度要比叶块和表块高得多。但根、枝块的修改频度,则又会远低于叶块、表块。
在这种情况下,如果每次查询根块、枝块都以独占模式获得 CBC Latch ,再以共享模式得到 Buffer Pin 锁,然后查询 Buffer 数据 。虽然这在 Buffer Pin 锁相关步骤不会产生等待,但由于 CBC Latch 是独占的,必然导致那里产生激烈的竞争。
如何进行优化调整呢?Oracle 公司的开发人员还是很聪明的,他们清楚,独占 CBCLatch 的目的,是为了保护修改 BH 中 Buffer Pin 锁状态的过程。如果不去修改 Buffer Pin 锁,那么对于 BH ,就只剩下读操作了。既然只有读,也就没必要以独占模式获得 CBC Latch 了。因此优化调整后的具体步骤如下。
步骤1:以共享方式获得CBC Latch(在前面方法中,是以独占方式获得Latch的),如图 11 所示。
图 11 共享 CBC Latch
步骤2:在共享 CBC Latch 保护下,搜索链表。查询 BH 中的 BA ,如图 12 所示。在此步骤中,不需要对 Buffer Pin 锁的状态进行修改。
图 12 搜索链表
步骤3:在共享 CBC Latch 的保护下,根据 BH 中 BA 地址,查询 Buffer中的 数据,如图 13 所示。
图 13 查询 Buffer 中的数据
步骤4:查询 Buffer 数据完毕,释放 CBC Latch,如图 14 所示。
图 14 释放 CBC Latch
看到和刚才的区别没?在此过程中,共享 CBC Latch 一直持续到读取 Buffer 数据结束,没有修改 Buffer Pin 锁的过程。
虽然 CBC Latch 加载的时间长了些,但由于是共享模式,在大量读取操作的环境中,可以有效降低竞争。索引根块、枝块的绝大多数操作都是读,除非发生了索引分裂。
除了普通索引的根块、枝块外,在有唯一索引、索引唯一扫描时,索引的根块、枝块,还有叶块、表块都将以共享 CBC Latch 的方式保护。
3)Cache Buffers Chain Latch 的竞争
在图 6 中已经有描述,一个 CBC Latch 可保护多个链表,而且它除了保护链表的访问以外,还要保护 BH 的读和修改操作。而一个链表中,可能会有多个 BH,这样算下来,一个 CBC Latch 除了操作多个链表以外,还要保护数目更多的 BH 。因此,当 CBC Latch 出现竞争时,可能是如下两种情况:
- 多个进程频繁地以不兼容的模式申请获得某一 CBC Latch ,访问此 CBC Latch 保护的不同链表和不同 BH 。
- 多个进程频繁地以不兼容的模式申请获得某一 CBC Latch ,访问此 CBC Latch 保护的同一链表下的同一 BH 。
在这两种情况中,第一种情况被称为热链竞争,第二种情况被称为热块竞争。
热链竞争最容易解决。多个进程其实访问的是不同的BH,只不过恰好这些 BH 在同一 CBC Latch 保护下(这种巧合的情况当然比较少见,但偶尔也会遇到),这时,解决方案很简单,可对两个隐藏参数中的一个进行修改,即 _db_block_hash_buckets 和 _db_block_hash_latches ,它们分别控制 HASHBucket 的数量和 CBC Latch 的数量。这样一来,BH 和 HASH Bucket 的对应关系就会被重新计算。原本在同一链表中的 BH,重新计算后很可能就不在同一链表中了。
除了以上所说的热链竞争外,第二种情况就属于“热块”竞争了。
热块竞争比较难解决,但好在这种竞争一般不会出现。以前索引的根块、枝块查询频度高,很容易导致热块竞争,但在 Oracle 9iR2 之后,Oracle 已经对这一部分进行了优化(前文已有过详细讨论),根块、枝块现在使用的都是共享模式的 CBCLatch 。因而,现在出现 CBC Latch 竞争的大多数情况,都是 SQL 语句执行计划不合理导致的,只需要调优 SQL 即可。但也有些情况是 SQL 调优所不能解决的。比如,某个应用中有一个很小的表,只有十几行数据,如此小的表没有为它建立索引,每次访问都是全表扫描。后来当查询压力逐渐加大时,这个表的块上就出现了大量的 CBC Latch 竞争。查看这个表的 SQL 查询,发现有很多 SQL 是等值查询,因此,这个问题的解决方案很简单,即建立一个唯一索引。
只要有锁,就有竞争。逻辑读时的锁,除了 CBC Latch ,还有 Buffer Pin 锁。Buffer Pin 锁的相关等待事件就是 Buffer Busy Waits 。
Buffer Pin 锁有两种模式:共享和独占,分别对应读、写两种操作。如果要读一个 Buffer,先要获得共享的 Buffer Pin 锁,这样才能读这个 Buffer。写则是要先获得独占 Buffer Pin 锁,然后才能修改 Buffer。
至于 Buffer Pin 锁的阻塞模式,写与写互相阻塞,这是毋庸置疑的。但读与写之间的阻塞模式,Oracle 10g 后的版本中就未再简单地让它们阻塞了事,而是很巧妙地对并发的读、写操作进行了优化。下面就一起来看看。假设 A 进程先发起读操作,B 进程后而后发起写操作,这样一来,B 进程的写会被 A 进程的读阻塞。但是,根据测试,B 并不会被 A 阻塞。过程如下:
步骤1:A 进程在 BH 中成功地设置了共享 Buffer Pin 锁。注意,此时 A 进程已经释放了 CBC Latch 。CBC Latch 的目的之一就是保护 Buffer Pin 锁的设置过程,这在前文中已经重点描述。Buffer Pin 锁设置成功了,当然就要释放 CBC Latch 。A 进程会在 Buffer Pin 锁 的保护下读 Buffer 中的数据,如图 15 所示。
图 15 设置 Buffer Pin 锁
步骤2:B 进程想修改 Buffer,它首先获得 CBC Latch,如图 16 所示。
图 16 B 进程获得 CBC Latch
步骤3:B 进程查看 BH 中 Buffer Pin 锁的状态,发现其他进程留下的 S 锁,如图 17 所示。B 进程会等待吗?当然不会。
图 17 B 进程查看 Buffer Pin 锁状态
步骤4:在这一步中 B 进程会做很多工作。它会在原来的 BH 中也留下一个共享的 Buffer Pin 锁,然后释放 CBC Latch 。在共享 Buffer Pin 锁的保护下,将原来的 Buffer 复制到 Buffer Cache 里另外的 Buffer 中(这个过程 Oracle 称为 Buffer 克隆,即 Buffer Clone),如图 18 所示。然后还要增加一个新的 BH,这一过程又要在 CBC Latch 的保护下进行。新的 BH 建立完毕后,释放 CBC Latch 。这一步完成后的最终结果如图 18 所示。HASH 链中多了一个一模一样的 BH,下面的 BufferCache 池中则多了一个一模一样的 Buffer 。
图 18 B 进程 Buffer 克隆
注意,在图 18 中 BH 中多了一行:STATUS 。顾名思义,STATUS 表示 Buffer 的状态。在 Buffer 克隆开始时,原 Buffer 的 BH 中 STATUS 值为 XCUR 。Buffer 克隆的目标 Buffer BH 中的 STATUS 没有值。
对于这个状态列的意义,可通过图 19 来介绍。
步骤5:在源 Buffer 已经成功复制到另外的 Buffer 中、原来的 CBC 链中增加一个新的 BH 后,从本步骤开始,首先获得 CBC Latch,修改源 BH 中的 STATUS 为 CR,新 BH 中的 STATUS 为 XCUR。同时,在新 BH 中,设置独占 Buffer Pin 锁,准备开始修改 Buffer 。到此为止,Buffer 克隆才算结束。
Buffer 克隆结束后,Buffer Cache 中多了一个新的 STATUS 为 XCUR 的 Buffer,原 Buffer 的 STATUS 则被改为 CR 。
CR 的本意为一致读(consist read),在这里,我们只需要知道,它不是当前块。XCUR 是当前块,代表它里面包含用户所有最新的修改。
图 19 STATUS 的意义
Oracle之所以能做到读不阻塞写,奥秘就在此。将 A 进程正在读的 Buffer 转为 CR 块,另外复制一个 Buffer 作为 XCUR 块。A 进程正在读 Buffer 数据时,它只持有 Buffer Pin 锁,并不持有 CBC Latch,因此 B 进程可以申请 CBC Latch,修改 A 进程正在读的 Buffer 的 BH,将它的状态改为 CR 。这对 A 进程的读操作并不会产生影响,因为 A 进程正在读的是 Buffer 数据,而 B 进程修改的是 BH 。
步骤6:释放 CBC Latch 。在独占 Buffer Pin 锁的保护下,修改对应 Buffer,如图 20 所示。
图 20 B 进程修改对应 Buffer
若这个时候正好又来个 C 进程,也要读 5 号文件中的 1234 号块,会是什么情况呢?
当然就要等待了。C 进程要在状态为 XCUR 的 BH 中加共享的 Buffer Pin 锁。但是这个 BH 中正有一个独占 Buffer Pin 锁,因此 C 进程被阻塞。这个等待事件当然就是大名鼎鼎的 Buffer Busy Waits 了。
这就是 Oracle 10g 后直到 Oracle 12c 中 Buffer Busy Waits 的重要改变:读不阻塞写,而写阻塞读。
因此,造成 Buffer Busy Waits 等待的“元凶”只能是 DML 语句。如果在 AWR 中看到 Buffer Busy Waits,那就从 DML 上找原因吧。
摘自:书籍《Oracle 内核技术揭密》