Latch详解
— MaxChou
本文以学习为目的,大部分内容来自网络转载。
什么是Latch
串行化
数据库系统本身是一个多用户并发处理系统,在同一个时间点上,可能会有多个用户同时操作数据库。多个用户同时在相同的物理位置上写数据时,不能发生互相覆盖的情况,这叫做串行化。串行化会降低系统的并发性,但这对于保护数据结构不被破坏来说则是必需的。在Oracle数据库中,通过闩锁(latch)、锁定(lock)、互斥(mutex)来实行串行化,保护数据结构一致性的。
Latch的定义和作用
Oracle数据库使用闩锁(latch)来管理SGA内存的分配和释放,Latch是用于保护SGA中共享数据结构的一种串行化锁定机制。Latch的实现是与操作系统相关的,尤其和一个进程是否需要等待一个latch、需要等待多长时间有关。
Latch是一种能够极快地被获取和释放的锁,它通常用于保护描述buffer cache中block的数据结构。
比如数据缓存中的某个块要被读取,我们会获得这个块的latch,这个过程叫做pin;另外一个进程恰好要修改这个块,他也要pin这个块,此时他必须等待。当前一个进程释放latch后才能pin住,然后修改。如果多个进程同时请求的话,他们之间将出现竞争,没有一个入队机制,一旦前面进程释放latch,后面的进程就蜂拥而上,没有先来后到的概念,这个和Lock是有本质区别的。这一切都发生的非常快,因为Latch的特点是快而短暂,当然这个只是大致过程。
如何获取Latch
任何时候只有一个进程可以访问内存中的某一个块,如果进程因为别的进程正占用块而无法获得Latch时,他会对CPU进行一次spin(旋转),时间非常的短暂,spin过后继续获取,不成功仍然spin,直到 spin次数到达阀值限制(这个由隐含参数_spin_count指定),此时进程会停止spin,进行短期的休眠,休眠过后会继续刚才的动作,直到获取块上的Latch为止。进程休眠的时间也是存在算法的,他会随着spin次数而递增,以厘秒为单位,如1,1,2,2,4,4,8,8...休眠的阀值限制由隐含参数_max_exponential_sleep控制,默认是2秒,如果当前进程已经占用了别的Latch,则他的休眠时间不会太长(过长会引起别的进程的Latch等待),此时的休眠最大时间有隐含参数_max_sleep_holding_latch决定,默认是4厘秒,这种时间限制的休眠又称为短期等待。
另外一种情况是长期等待锁存器(Latch Wait Posting),此时等待进程请求Latch不成功,进入休眠,他会向锁存器等待链表(Latch Wait List)压入一条信号,表示获取Latch的请求,当占用进程释放Latch时会检查Latch Wait List,向请求的进程传递一个信号,激活休眠的进程。Latch Wait List是在SGA区维护的一个进程列表,他也需要Latch来保证其正常运行,默认情况下share pool latch和library cache latch是采用这个机制,如果将隐含参数_latch_wait_posting设置为2,则所有Latch都采用这种等待方式,使用这种方式能够比较精确的唤醒某个等待的进程,但维护Latch Wait List需要系统资源,并且对Latch Wait List上Latch的竞争也可能出现瓶颈。
如果一个进程请求、旋转、休眠Latch用了很长时间,他会通知PMON进程,查看Latch的占用进程是否已经意外终止或死亡,如果是,则PMON会清除释放占用的Latch资源。
现在大家可以明白,对Latch获取的流程了,请求-SPIN-休眠-请求-SPIN-休眠...占用。这里有人会问为什么要SPIN,为什么不直接休眠等待?这里要明白休眠意味着什么,他意味着暂时的放弃CPU,进行上下文切换(context switch),这样CPU要保存当前进程运行时的一些状态信息,比如堆栈、信号量等数据结构,然后引入后续进程的状态信息,处理完后再切换回原来的进程状态,这个过程如果频繁的发生在一个高事务,高并发进程的处理系统里面,将是个很昂贵的资源消耗,所以他选择了spin,让进程继续占有CPU,运行一些空指令,之后继续请求,继续spin,直到达到_spin_count值,这时会放弃CPU,进行短暂的休眠,再继续刚才的动作。
当尝试获得Latch的时候,可能会消耗大量的CPU时间,系统看上去很忙,但是并没有做多少实际工作。
系统发生关于Latch的等待是没法避免的,因为这是Oracle的运作机制,当你看到很高的Latch get时并不意味着你的系统需要调整,有时候很高的get值背后只有很短的等待时间,我们调整的对象应该以消耗的时间来圈定,而不是只看到一个很高的获取次数值。当然,获取值异常的高出别的等待时间几十万倍时我们还是要关心的,Oracle关于Latch的等待非常繁多,主要的包括share pool,library cache,cache buffer chains,buffer busy wait。
Oracle使用两种数据结构来进行shared pool的并发控制:lock和pin。Lock比pin具有更高的级别,Lock在handle上获得,在pin一个对象之前,必须首先获得该handle的lock。
Lock主要有三种模式:Null、share、Exclusive。在读取访问对象时,通常需要获取Null(空)模式以及share(共享)模式的锁定;在修改对象时,需要获得Exclusive(排他)锁定。
在锁定了Library Cache对象以后,一个进程在访问之前必须pin该对象。同样pin有三种模式:Null、shared、exclusive。只读模式时获得共享pin,修改模式获得排他pin。
通常我们访问、执行过程、Package时获得的都是共享pin,如果排他pin被持有,那么数据库此时就要产生等待。
PS :
latch是用于保护SGA区中共享数据结构的一种串行化锁定机制。它不仅仅用于buffer cache, 还用于shared pool以及log buffer等。
闩锁和锁定既有相同点又有不同点。相同点在于它们都是用于实现串行化的资源。而不同点则在于闩锁(Latch)是一个低级别、轻量级的锁,获得和释放的速度很快,以类似于信号灯的方式实现。而锁定(Lock)则可能持续的时间很长,通过使用队列,按照先进先出的方式实现。也可以简单地理解为闩锁是微观领域的,而锁定则是宏观领域的。
Latch的类型:
Willing-to-wait类型Latch:
如果一个进程在第一次尝试中没有获得该Latch,那么它会等待并再次尝试一次,如果
经过_spin_count次争夺不能获得Latch,该进程就会转入睡眠状态,睡眠结束后,按
顺序重复之前的步骤,并且睡眠时间会越来越长。
Immediate类型Latch:
如果该Latch不能立即得到,该进程不会等待而是继续执行其他操作。
术语解惑
-转自Askmaclean的回答
首先理解cache buffer chain(hash chain) 和 cache buffer bucket (hash bucket)。
对于Buffer cache管理而言 oracle所需要解决的问题包括几个:
- 如何快速定位一个data buffer header?
因为Buffer cache中的 data buffer header是非常多的,若为了找一个data buffer header
而去对所有的buffer header都扫描一遍 ,那将是非常低效的。举个例子来说,服务进
程要有读取datafile 5 block 10的需求,这个时候难道服务进程一开始就知道data file 5
的block 10在是不是在Buffer cache中,在Buffer cache中的哪里?这些信息Server
process都是不知道的。如果 data buffer header被使用普通的双向链表组织,那么如果
要确定一个data buffer 是否在Buffer Cache中,那么需要把这个双向链表上所有的
buffer header都查看一遍,这是十分低效的。
- 对data buffer header高效的并发管理,避免出现争用:
为了实现高效管理data buffer header的目的,oracle使用hash buckets的结构来组织data
buffer header,通过对data buffer header的不同rdba和class做HASH算法来实现对buffer
header的高效管理,通俗来说HASH做的就是一件事,例如data file 4 上的 block 18 和
block 19是应用经常要访问的热快,经过HASH算法之后这2个块就不会落在同一个
HASH Buckets中,这样避免了对同一条hash chain的争用。
oracle又通过hash chains( cache buffer chain) 将一个bucket上的buffer header串起来,
注意:同一个data block在oracle中可能会有多个buffer ,分别对应为一个current block
和可能的多个cr block,这些block都同一条cache buffer chains上。
为了实现对cache buffer chains的并发控制需要用到latch来管理,所以会有cache buffer
chains latch。
CBC 的latch管理是Buffer cache Internal的主要部分,值得研究 但是并非一段文字所能全部描述。
你也可以参考下面这个图示:
与BUFFER CACHE相关的Latch
BUFFER CACHE相关的Latch
Latch:cache buffers lru chain
Latch:cache buffers chains
既然Latch是用来保护共享内存块不被并发性破坏,那就需要了解Buffer Cache的原理,进而得知需要用到Latch的情况,才能有应对的方案。
Buffer Cache是用来缓存数据块的地方,那么数据的查询和修改都要通过它来完成,接下来看访问数据的流程:
当一个进程想要访问数据时,首先要查找Buffer Cache中是否已经存在:
(Y)->如果数据在Buffer Cache中存在,则根据数据的状态来判断是否可以直接访问,还是需要构造一致性读块(CR块);
(N)->如果数据在Buffer Cache中不存在,则需要从磁盘中读取数据块到Buffer Cache去,这个时候需要在Buffer Cache中寻找足够的内存空间来读取相关数据块。
如何确定Buffer Cache中是否已经存在需要的数据块
Y->如果数据块在Buffer Cache中存在的情况
- 根据要查找的数据块的DBA(Data Block Address)等信息,通过Hash算法(Hash Bucket=MOD(Data Block Address,_DB_BLOCK_HASH_BUCKETS)),得到该数据块所在的Hash Bucket;
- 定位到对应的Hash Bucket上,在该Hash Bucket对应的Cache Buffers Chain中加上Cache Buffers Chains Latch,然后从Cache Buffers Chain对于的第一个Data Buffer Header开始扫描查找,直至最后一个。在这个扫描查找过程中,为了防止对Cache Buffers Chain产生并发访问,将一直持有Cache Buffers Chains Latch;
在Cache Buffers Chain上查找的具体逻辑如下,根据我手画的图理解:
1.比较Data Buffer Header上所记录的Block地址,不符合条件的就跳过此Data Buffer Header; |
|
2.跳过status为CR的Data Buffer Header; |
|
3.如果Data Buffer Header状态为reading则等待,直到状态改变后比较Buffer Header记录的Block地址是否符合; |
|
4.若发现Block地址符合的Data Buffer Header,查该Data Buffer Header是否位于正在使用的list上,如果是,则判断已存在的lock mode,与要求的lock mode是否兼容,如果兼容则返回该Data Buffer Header中记录的Buffer地址,将当前process id放入Data Buffer Header所处的正在使用的list上; |
|
5.如果lock mode不兼容,用Data Buffer Header所指向的Buffer中的内容构建一个xcurrent的Buffer和一个CR状态的Data Buffer Header(指向新建立xcurrent状态的复制Buffer)。 |
|
6.搜索完整个Hash Chain还未发现需要的Data Buffer Header,从disk读取数据块,读入到Buffer Cache中,相应的Data Buffer Header挂在Cache Buffers Chain上。 |
|
Ps: Buffer Header指向的Buffer的6种状态:(可以参照V$BH的status,v$bh:非常详细地记录了数据块在数据缓冲区内的使用情况,一条记录对应一个block的详细记录。 v$bh来自于基表x$bh与x$le) |
1. Free:可以被重用的Block; |
2. xcurrent:以exclusive方式获取的当前模式的Block(insert、update、delete时产生), Scurrent: 可以与其他instance共享的当前模式Block; |
|
3. CR:一致读块,永远不会写入disk; |
|
4. reading:正从disk读取出来的块; |
|
5. mreciver:正在进行介质恢复的Block; |
|
6. ircovery:正在进行instance recovery的Block。 |
- 获得第二步中查到的可以直接访问的Data Buffer Header或者构造一致性读后的Data Buffer Header中的Buffer地址,到Buffer Memory查找对应数据块。
N->如果数据块在Buffer Cache中不存在(在Hash Chain中查找不到对应Data Buffer
Header)
- 需要从数据文件(磁盘)中读取数据块到Buffer Cache中去。这时候,需要在Buffer Cache中寻找足够的内存空间来存放相关的数据块。
如何才能快速查询到数据块
Buffer Cache是很大的,如果一个Buffer一个Buffer的扫描是相当耗费资源和查询时间的,所以要通过类似目录(Hash Bucket)的方法,让数据库能快速定位数据块的位置。
下面是关于Buffer Cache的示意图:
先看以下几点:
- 图中右边有一块Buffers Memory,其中每一小格代表一个Buffers(用来存放数据文件中读取的数据块Block);
- 图中左边有许多Data Buffer Header用虚线指向Buffers Memory中相应的Buffer;
- 图中左边有许多实线箭头,这些箭头(其实就是数据结构的链表结构中的指针)将不同的Data Buffer Header连接成一条Hash Chain,也就是Cache Buffers Chain(双向链表);
- Hash Bucket,其实这只是一个逻辑概念,即每一个Hash Bucket都会有一条Hash Chain来将Data Buffer Header(按照Hash算法分类后)连接起来,并由一个Cache Buffers Chains Latch来进行管理其并发操作;
- 每当将一个Block读入到Buffer Cache的时候,首先会构造一个与之相对应的Data Buffer Header,然后根据Hash算法(Hash Bucket=MOD(Data Block Address,_DB_BLOCK_HASH_BUCKETS)),将Data Buffer Header放到对应的Hash Bucket的Cache Buffers Chain中去,并在Data Buffer Header中存放如下信息:
1.存放该Block在Buffer Cache中实际存储地址; |
2.存放该Block的类型(data,segment header,undo header,undo Block等类型); |
3.由于此Data Buffer Header 所在的Cache Buffers Chain(Hash Chain),是通过在Data Buffer Header保存指向前一个Data Buffer Header的指针和指向后一个Data Buffer Header的指针方式实现,所以还存指针; |
4.存储lru,lruw,ckptq,fileq等队列,一样是通过记录前后Data Buffer Header指针方式实现; |
5.当前该Data Buffer Header所对应的数据块的状态以及标记; |
6.该Data Buffer Header被访问的次数(touch次数); |
7.正在等待该Data Buffer Header的进程列表(waiter list)及正在使用此Data Buffer Header的(user list); |
- Hash Latch,即Cache Buffers Chain Latch,Buffer Cache中Hash Bucket的个数由隐含参数_db_block_hash_buckets决定,Cache Buffers Chains Latch的个数由隐含参数_db_block_hash_latches决定。
在上面的Buffer Cache示意图中可以看到两条链(LRU和LRUW),这两条链分别将Data Buffer Header连接起来,和Cache Buffers Chain类似:
LRU和LRUW的作用:
- LRU表示Least Recently Used,也就是指最少最近使用的Data Buffer Header链表,LRU链表串联起来的Data Buffer Header都指向可用数据块(Free Buffer);
- LRUW则表示Least Recently Used Write,也叫做Dirty List,即脏数据块链表,LRUW串联起来的都是修改过但是还没有写入数据文件的数据块所对应的Data Buffer Header(Dirty Buffer);
- 一个Data Buffer Header 要么在LRU上,要么在LRUW上,不能同时存在于两个链表上。
所以当查找数据块在Buffer Cache中不存在的时候(即Hash Chain中查找不到对应的
Data Buffer Header的情况下):就要扫描LRU List寻找Free的Buffer,在扫描过程将
持有Cache Buffers Lru Chain Latch(其Latch数量由隐含参数_db_block_lru_latches决
定),扫描过程中会把已经修改过的Buffer移动到LRUW链表上;
- 找到足够的Buffer之后,将数据块读入到Buffer Cache,构造一个与之对应的Data Buffer Header,然后根据Hash算法(Hash Bucket=MOD(Data Block Address,_DB_BLOCK_HASH_BUCKETS)),将Data Buffer Header放到对应的Hash Bucket的Cache Buffers Chain中去,并在Data Buffer Header中存放相关的信息。
在Buffer Cache中查找数据块的总流程:
可能导致Latch争用的两种情况
A. 某一进程过长时间的持有Latch,导致其他进程不能正常的得到Latch,只能等待;
B. 可能存在的大量的Latch请求。
4.1 如果出现Cache Buffers Chains Latch严重争用,根据以上原理,那么可能有如下原因:
- 当多个会话重复访问一个或多个由同一个子Cache Buffers Chains Latch保护的块是热点块(可以关注X$BH中的TCH字段);
- 大量并发执行的低效SQL,低效的SQL通常需要获取大量的逻辑读,而得到一次逻辑IO就获得一次Cache Buffers Chains Latch;
- Hash Bucket中存在长的Cache Buffers Chains,导致查询数据块时候,长时间持有Latch。
4.2 如果出现Cache Buffers Lru Chain Latch严重争用,那么可能有如下原因:
- 可能Buffer Cache分配的空间不够,导致读数据到Buffer Cache的时候,不断的扫描LRU List。
- 测试:模拟Cache Buffers Chains Latch争用
5.1 创建表test,总共一条记录,共1个Block
SQL> select * from test;
ID
----------
1
SQL> select dbms_rowid.ROWID_RELATIVE_FNO(rowid) file#,dbms_rowid.ROWID_BLOCK_NUMBER(rowid) block#
from test;
FILE# BLOCK#
---------- ----------
1 35337
5.2 创建存储过程用于模拟一个Block不断的查询:
SQL> create or replace procedure sp_test_cbc_latch is
2 i number;
3 begin
4 loop
5 select id into i from test;
6 end loop;
7 end;
8 /
Procedure created.
开始执行存储过程之前的笔记本和虚拟机CPU使用情况:
5.3 在session 482上执行该存储过程:
SQL> select * from v$mystat where rownum=1;
SID STATISTIC# VALUE
---------- ---------- ----------
482 0 0
SQL> exec sp_test_cbc_latch;
5.4 在另一个会话489上查询482会话的event:
SQL> select * from v$mystat where rownum=1;
SID STATISTIC# VALUE
---------- ---------- ----------
489 0 0
select sid,event,P1,P1RAW,P1TEXT,P2,P2TEXT from v$session where sid in (482,489);
SID EVENT P1 P1RAW P1TEXT P2 P2TEXT
----------------------------- ---------- ---------------- --------------- ---------- ----------------------------------------------------------------------------------
482 SQL*Net message from client 1650815232 0000000062657100 driver id 1 #bytes
489 SQL*Net message from client 1650815232 0000000062657100 driver id 1 #bytes
在执行期间可以看到Cache Buffers Lru Chain Latch都是GET成功的,不存在竞争:
SQL> select gets,misses,sleeps,spin_gets,wait_time
2 from v$latch
3 where name=‘cache buffers chains‘;
GETS MISSES SLEEPS SPIN_GETS WAIT_TIME
---------- ---------- ---------- ---------- ----------
369055278 10736406 1399 10735373 1113539
5.5 再开一个会话27执行上面的存储过程:
SQL> select * from v$mystat where rownum=1;
SID STATISTIC# VALUE
---------- ---------- ----------
27 0 0
SQL> exec sp_test_cbc_latch;
SQL> select sid,event,P1,P1RAW,P1TEXT,P2,P2TEXT from v$session where sid in (482,489,27);
SID EVENT P1 P1RAW P1TEXT P2 P2TEXT
---------- ------------------------------ ---------- ---------------- --------------- ---------- ---------------
27 cursor: pin S 3777061920 00000000E1216420 idn 2 value
482 cursor: pin S 3777061920 00000000E1216420 idn 1.1596E+11 value
489 SQL*Net message to client 1650815232 0000000062657100 driver id 1 #bytes
select gets,misses,sleeps,spin_gets,wait_time from v$latch where name=‘cache buffers chains‘
GETS MISSES SLEEPS SPIN_GETS WAIT_TIME
---------- ---------- ---------- ---------- ----------
647782348 28420853 2277 28419082 1586813
- 可以看到出新latch:cache buffers chains等待事件,这就是两个会话同时要访问同一个Block,这个时候在一个会话持有Latch的时候,另一个会话必须spin等待获得Latch;
- 同时也能看到latch:library cache等待事件,这是由于在共享池进行软解玺的时候需要互动额library cache latch来扫描library cache中相应Bucket的Chain来获得执行计划来执行SQL,因为并发性高,导致library cache latch的争用;
- 我做实验的时候并没有出现而是出现下面的等待事件,也会看到latch:share pool或者cursor:pin S等待事件:
SQL> select sid,event,P1,P1RAW,P1TEXT,P2,P2TEXT from v$session where sid in (482,489,27);
‘
SID EVENT P1 P1RAW P1TEXT P2 P2TEXT
---------- ------------------------------ ---------- ---------------- --------------- ---------- ---------------
27 latch: cache buffers chains 2592449584 000000009A85A430 address 155 number
482 cursor: pin S 3777061920 00000000E1216420 idn 1.1596E+11 value
489 SQL*Net message to client 1650815232 0000000062657100 driver id 1 #bytes
SQL> select SID,EVENT,P1,P1RAW,P2,P2TEXT from v$session where sid in (489,482,27);
SID EVENT P1 P1RAW P2 P2TEXT
---------- ------------------------------ ---------- ---------------- ---------- ---------------
27 SQL*Net message from client 1650815232 0000000062657100 1 #bytes
482 latch: shared pool 1611695912 0000000060108728 307 number
489 SQL*Net message to client 1650815232 0000000062657100 1 #bytes
SQL> /
SID EVENT P1 P1RAW P2 P2TEXT
---------- ------------------------------ ---------- ---------------- ---------- ---------------
27 cursor: pin S 3777061920 00000000E1216420 2.0702E+12 value
482 cursor: pin S 3777061920 00000000E1216420 1.1596E+11 value
489 SQL*Net message to client 1650815232 0000000062657100 1 #bytes
- 可以看到cache buffers lru latch都是get出现严重争用,出新大量misses、sleeps、spin_gets:
SQL> select GETS,MISSES,SLEEPS,SPIN_GETS,WAIT_TIME from v$latch where name=‘cache buffers chains‘;
GETS MISSES SLEEPS SPIN_GETS WAIT_TIME
---------- ---------- ---------- ---------- ----------
2145962921 114891477 6075 114886552 3355656
所以当存在大量latch争用的时候会大量消耗系统CPU资源。
与Shared Pool相关的Latch
- Shared Pool相关的主要Latch:
Latch:shared pool
Latch:library cache
Oracle通过Shared Pool来实现SQL共享,减少硬解析等,而SQL的相关信息,如SQL语句文本、执行计划都存放在Shared Pool的library cache部分。
- Library Cache的结构如下图:
可以看到其结构和Buffer Cache类似,为了能够在Library Cache中快速查找到对应的SQL,也是将不同的SQL语句通过Hash函数Hash后放置到对应的Hash Bucket来保存。
Object Handle:
- 这个块就是所谓的Library Cache Object Handle,这个Handle描述Library Cache中对象的一些属性,如名称(Name),所属的命名空间(Namespace)、标记(Flags)、指向对象所处的内存地址的指针(Heap0)等,对应的SQL来说,这个可以算是父游标;
- Heap 0用来存放与对象有直接关系的一些信息,比如对象类型、对象相关的表、实际的执行计划等;
- 同一个Hash Bucket中的Object Handle相互链接形成一条Chain/
盖国强老师的博客上也有一张简洁的图:
- 通过了解SQL执行过程来了解Shared Pool的相关Lathch:
- 当客户端执行一条SQL,这时候Oracle首先将SQL文本转换成ASCII值,然后根据Hash函数计算该SQL对应的Hash Value;
- 根据得到的Hash Value到Library Cache中查找对应的Bucket,然后查找Bucket里是否存在该SQL:
(Y)->如果存在,则接下来查找对应的子游标,这个时候将一直持有Library Cache Latch,直到找到对应的执行计划,然后释放Latch(软解析);
(N)->如果不存在,就要去Shared Pool里面获得可用空间,来生成对应的Library Cache对象,这个时候就要获得Shared Pool Latch在Shared Pool的Free list(Shared Pool通过Free List管理Free Chunk)查找可用的空间,之后释放Shared Pool Latdh,接下来就开始进行硬解析过程,将执行解析后的执行计划等信息记录到Library Cache中,这个过程消耗大量CPU,同时将一直持有Library Cache Latch,一直到硬解析结束(硬解析);
- 根据获得的执行计划,剋是执行SQL,如:到Buffer Cache查询数据块等。
- 整个逻辑如下:
- 当出现Latch严重争用的时候:
5.1 如果同时出现大量的Shared Pool Latch和Library Latch的话,根据上面的逻辑说明数据库中存在大量硬解析,这个时候就要查找哪些SQL是否没有绑定变量。
5.2 如果只是出现大量Library Cache Latch的话,那么可能有两种情况:
- 当持有Library Cache Latch查找Bucket对应的Chain时候,发现存在高Version count的SQL,这个时候就要扫描这些对应的子游标,整个过程一直持有Latch,导致其他会话获取不到Latch进行操作;
- 大量的并发请求,而且不能实现SQL一次Parse Call多次Execution的情况。
- 测试模拟硬解析和SQL的V而死哦那Count高的情况:
6.1 Oracle 10g以后都有方法可以让SQL产生很多的子游标,并且具备下面几种条件:
- cursor_sharing=similar
- 收集了列上的histogram
- SQL中使用了此列作为条件,并且条件是“等于”
- 这个SQL是没有绑定变量的
这时候,Oracle会认为每条SQL的literal变量都是unsafe的,因此就不重用以前的cursor,而产生一个version,重新硬解析一次。
6.2 首先创建表test1(session1:sid=462),然后在列上收集直方图,设置cursor_sharing=similar
SQL> select * from v$mystat where rownum=1;
SID STATISTIC# VALUE
---------- ---------- ----------
462 0 0
create table test1 as select rownum as hisgron_col,rownum col from dba_objects;
exec dbms_stats.gather_table_stats(user,‘test1‘,method_opt=>‘for columns hisgron_col size 3‘);
SQL> select column_name,num_buckets,histogram from user_tab_col_statistics where table_name=‘TEST1‘;
COLUMN_NAME NUM_BUCKETS HISTOGRAM
------------------------------ ----------- ---------------
HISGRON_COL 3 HEIGHT BALANCED
SQL> alter session set cursor_sharing=similar;
Session altered.
SQL> alter system flush shared_pool;
System altered.
6.3 开启另一个会话(session2:sid=484),以上面同样的操作创建表test2:
SQL> select * from v$mystat where rownum=1;
SID STATISTIC# VALUE
---------- ---------- ----------
484 0 0
两个会话同时执行下面的存储过程,模拟并发:
Session1:sid 462
begin
for i in 1..50000 loop
execute immediate ‘select * from test1 where hisgrom_col=‘||i;
end loop;
end;
Session2:sid 484
SQL> begin
2 for i in 1..50000 loop
3 execute immediate ‘select * from test2 where hisgrom_col=‘||i;
4 end loop;
5 end;
6 /
这样写法的目的一个是为了不实现绑定变量,二是为了在Library Cache中快速产生高Version Count的SQL。
6.4 另开一个会话查看event:
SQL> select sid,event,p1,p1text,p2,p2text from v$session where sid in (462,484);
SID EVENT P1 P1TEXT P2 P2TEXT
---------- ------------------------------ ---------- --------------- ---------- ---------------
462 latch: shared pool 1611695912 address 307 number
484 latch: shared pool 1611695912 address 307 number
SID EVENT P1 P1TEXT P2 P2TEXT
---------- ------------------------------ ---------- --------------- ---------- ---------------
462 latch: shared pool 1611695912 address 307 number
484 latch: shared pool 1611695912 address 307 number
SQL> /
SID EVENT P1 P1TEXT P2 P2TEXT
---------- ------------------------------ ---------- --------------- ---------- ---------------
462 latch: row cache objects 2547220064 address 280 number
484 latch: shared pool 1611695912 address 307 number
SQL> /
SID EVENT P1 P1TEXT P2 P2TEXT
---------- ------------------------------ ---------- --------------- ---------- ---------------
462 latch: shared pool 1611695912 address 307 number
484 latch: shared pool 1611695912 address 307 number
SQL> /
SID EVENT P1 P1TEXT P2 P2TEXT
---------- ------------------------------ ---------- --------------- ---------- ---------------
462 latch: shared pool 1611695912 address 307 number
484 latch: shared pool 1611695912 address 307 number
从上面的过程看到,大量的硬解析导致严重的latch:shared pool,有一个latch:row cache objects,没有Library cache latch。
6.5 下面看看Library Cache中的SQL情况
1 select * from
2 (select sql_id,child_number,child_latch,executions,sql_text from v$sql
3 where sql_text like ‘%select * from test1 where hisgrom_col%‘
4 and sql_text not like ‘%v$sql%‘
5 and sql_text not like ‘%begin%‘
6 order by child_number desc)
7* where rownum < 10
SQL> /
SQL_ID CHILD_NUMBER CHILD_LATCH EXECUTIONS SQL_TEXT
------------- ------------ ----------- ---------- --------------------------------------------------
80bd9652bw00x 0 0 1 select * from test1 where hisgrom_col=48841
63axfanq9n04r 0 0 1 select * from test1 where hisgrom_col=49758
d6jrbkn0bw056 0 0 1 select * from test1 where hisgrom_col=48920
6q0x5j3h6c07a 0 0 1 select * from test1 where hisgrom_col=49215
6y1japnuuc09n 0 0 1 select * from test1 where hisgrom_col=49663
9nqm8puar00dt 0 0 1 select * from test1 where hisgrom_col=49001
dmumtqwykh0g5 0 0 1 select * from test1 where hisgrom_col=48844
dyra2169kc0n2 0 0 1 select * from test1 where hisgrom_col=48685
054uu8x52c0p1 0 0 1 select * from test1 where hisgrom_col=49695
9 rows selected.
SQL> select sql_id,hash_value,address,version_count from v$sqlarea where sql_id = ‘80bd9652bw00x‘;
SQL_ID HASH_VALUE ADDRESS VERSION_COUNT
------------- ---------- ---------------- -------------
80bd9652bw00x 1153302557 000000009345A768 1
可以看到SQL的Version_Count很高,而且V$SQL视图里面也能查到对应的子游标,为什么V$SQL最大的Child_Number和V$SQLAREA里的Version_Count值不同,应该是有些子游标被age out Share Pool的原因吧。
6.6 模拟高并发下对高Version Count SQL的查询:
在session1:sid=462中和session2:sid=484中一起执行下面SQL:
SQL> begin
2 for i in 1..500000 loop
3 execute immediate ‘select * from test1 where hisgrom_col =1‘;
4 end loop;
5 end;
6 /
查看session对应的event:
SQL> /
SID EVENT P1 P1TEXT P2 P2TEXT
---------- ------------------------------ ---------- --------------- ---------- ---------------
462 cursor: pin S 1125210909 idn 2.0788E+12 value
484 cursor: pin S 1125210909 idn 1.9843E+12 value
可以看到出现大量cursor:pin s。
与Log Buffer相关的Latch
- Log Buffer相关的主要Latch有:
Latch:redo copy
Latch:redo allocation latch
当一个进程在修改数据时候将会产生redo,这个redo首先在PGA中保存,然后进程需要获取redo copy latch(这个Latch的个数有隐含参数_log_simultaneous_copies决定),当获得redo copy latch后,进程接着获取redo allocation latch来分配redo log buffer中的空间,空间分配完成后,释放redo allocation latch,然后进程把PGA中临时存放的redo信息复制到redo log buffer ,复制完成后,释放redo copy latch。
附录
相关视图
V$LATCH
shows aggregate latch statistics for both parent and child latches, grouped by latch name.
V$LATCH_CHILDREN
contains statistics about child latches. 1.5.3 v$latch_misses :
This view contains statistics about missed attempts to acquire a latch.
V$LATCHNAME
contains information about decoded latch names for the latches shown in V$LATCH.
V$event_name
V$session
V$mystat
V$sql
V$sqlarea
我们可以通过对v$latch, v$latchholder, v$latchname的查询获得有关latch信息,例如:
/* 已知一个latch地址,找到latch名字 */
col name for a40
select a.name from v$latchname a, v$latch b
where b.addr = ‘&addr‘
and b.latch#=a.latch#;
/* 显示系统范围内的latch统计 */
column name format A32 truncate heading "LATCH NAME"
column pid heading "HOLDER PID"
select c.name,a.addr,a.gets,a.misses,a.sleeps,
a.immediate_gets,a.immediate_misses,b.pid
from v$latch a, v$latchholder b, v$latchname c
where a.addr = b.laddr(+)
and a.latch# = c.latch#
order by a.latch#;
/* 由latch名称显示对latch的统计 */
select c.name,a.addr,a.gets,a.misses,a.sleeps,
a.immediate_gets,a.immediate_misses,b.pid
from v$latch a, v$latchholder b, v$latchname c
where a.addr = b.laddr(+) and a.latch# = c.latch#
and c.name like ‘&latch_name%‘ order by a.latch#;
latch有40余种,但作为DBA关心的主要应有以下几种:
Cache buffers chains latch: 当用户进程搜索SGA寻找database cache buffers时需要使用此latch。
Cache buffers LRU chain latch: 当用户进程要搜索buffer cache中包括所有 dirty blocks的LRU (least recently used) 链时使用该种latch。
Redo log buffer latch: 这种latch控制redo log buffer中每条redo entries的空间分配。
Row cache objects latch: 当用户进程访问缓存的数据字典数值时,将使用Row cache objects latch。
下面我们将着重介绍一下如何检测和减少redo log buffer latch的冲突。对redo log buffer的访问是由redo log buffer latch来控制的,这种latch有两种类型, redo allocation latch和redo copy latch。
Redo allocation latch控制redo entries在redo log buffer中的空间分配。Oracle的一个用户进程只有得到redo allocation latch后才能为redo entries在redo log buffer中分配空间,又由于一个instance只有一个redo allocation latch,所以一次只有一个用户进程在buffer中分配空间。当用户进程获得latch后,首先为redo entry分配空间,然后进程继续持有latch并拷贝entry到buffer中,这种拷贝称为“在redo allocation latch上的拷贝”(copying on the redo allocation latch),拷贝完毕后,用户进程释放该latch。
一个“在redo allocation latch上的拷贝”的redo entry的最大值是由初始化参数LOG_SMALL_ENTRY_MAX_SIZE定义的,根据操作系统的不同而不同。
Redo Copy Latch只应用于多CPU的系统。在多CPU的instance中,如果一个redo entry太大,超过了LOG_SMALL_ENTRY_MAX_SIZE定义值,则不能进行“在redo allocation latch上的拷贝”, 此时用户进程必须获取redo copy latch。一个instance中可以有多个redo copy latch,其数目由初始参数LOG_SIMULTANEOUS_COPIES决定,缺省值为CPU数目。
在单CPU情况下,不存在redo copy latch,所有的redo entry无论大小, 都进行“在redo allocation latch上的拷贝”。
对redo log buffer的过多访问将导致redo log buffer latch的冲突,latch冲突将降低系统性能,我们可通过如下查询来检测这种latch冲突:
col name for a40
SELECT ln.name,gets,misses,immediate_gets,immediate_misses
FROM v$latch l,v$latchname ln
WHERE ln.name IN(‘redo allocation‘,‘redo copy‘) AND ln.latch#=l.latch#/
若misses与gets的比例超过1%或immediate_misses与(immediate_gets+immediate_misses)比例超过1%时,应考虑采取措施减少latch的冲突。
大多数的redo log buffer latch冲突是在多个CPU情况下,两个或多个Oracle进程试图同时得到相同的latch发生的。由于一个instance只有一个redo allocation latch,为减少redo allocation latch的冲突,应减少单个进程持有latch的时间,这可以通过减小初始参数LOG_SMALL_ENTRY_MAX_SIZE以减小redo entry的数目和大小来实现。如果观察到有redo copy latch冲突,可以通过增大LOG_SIMULTANEOUS_COPIES 初始参数来加大latch数目,其缺省值为CPU数目,最大可增大到CPU数目的两倍。