3.2 library cache的并发控制
由于library cache是可以被所有进程同时访问并修改的,因此就必然存在一个并发控制的问题。比如对于前面我们举的如图九所示的例子来说,第一次使用number类型的绑定变量执行的SQL语句产生的游标挂在bucket 64367上。而当我们第二次使用varchar2类型的绑定变量再次执行该SQL语句时,oracle需要访问该bucket 64367上的句柄,发现不能共用执行计划时,还要修改该句柄,向CHILDREN部分添加一条指向另外一个子游标的句柄。在很多进程并发执行的情况下,那么当修改挂在bucket 64367上的句柄的时候,必须防止其他进程同时修改所访问的句柄。因为如果不防止这种情况的出现,那么假如这时正好也有一个进程也是使用varchar2类型的绑定变量执行该相同的SQL时,那么也会向CHILDREN部分添加一条子游标的记录,那么CHILDREN部分就会出现两条varchar2的执行计划,而实际上这两个执行计划是一样的,是完全可以合二为一的,这样也就达不到SQL共享的目的。同时还要考虑,当oracle在向某个heap(比如heap 0和heap 6)填入SQL文本、执行计划等数据的过程中,要防止该heap又被其他进程分配掉的情况出现。如果不防止的话,那这个heap的数据就被两个进程同时写,那里面的数据一定是混乱而无法使用的了。
为了有效的解决上面所说的并发性的问题,oracle使用三种结构来完成对library cache的并发控制:lock、pin和library cache latch。简单来说,进程如果要访问或者修改library cache里的对象,首先必须获得library cache latch,然后获得handle上的lock,最后获得heap上的pin,访问或修改结束以后,释放pin、lock和latch。
lock是落在library cache里的对象句柄上的,用来管理并发性。按照前面所说的例子,当多个进程同时修改bucket 64367上的句柄的时候,只有一个进程能够获得该句柄上的lock,其他进程必须等待(体现的等待事件就是library cache lock)。同时,尝试获得某个句柄上的lock也是将游标句柄对象加载到shared pool里的唯一方式。也就是说,当客户端发出某个SQL语句时,oracle对该SQL语句运用hash函数生成hash值,然后到该hash值所对应的library cache的bucket里试图找到对应的句柄并lock该句柄时,如果发现该句柄不存在(可能是由于该SQL语句是一条全新的SQL语句,或者以前该SQL语句执行过但是现在被交换出了library cache),则会将该SQL游标所对应的句柄加载到library cache里。
目前有三种lock模式,分别是:share、exclusive和null。如果某个进程只是要读取句柄里的信息时,会对该句柄添加share模式的lock,比如当编译某个存储过程时,进程会去读取该存储过程所引用的子存储过程等,这时其他进程可以对该相同的句柄添加share和null模式的lock;如果某个进程需要修改对象里的信息时,就会对该句柄添加exclusive模式的lock,比如删除某个存储过程就会添加exclusive模式的lock,这时其他进程只能对该相同的句柄添加null模式的lock;null模式的lock比较特殊,在任何可以执行的对象上(比如存储过程、视图、函数等等)都存在该null模式的lock。你可以随意打破该模式的lock,这时该lock所在的对象就失效了,需要重新编译。当SQL开始解析时,或获得null模式的lock,然后会一直加在该对象上,直到某些会引起对象失效的DDL发生在对象或对象所依赖的其他对象上,这时该lock被打破。当发生null模式的lock时,其他进程可以对该相同的句柄添加任何模式的lock。
而pin则是落在heap上的,用来防止多个进程同时更新同一个heap。pin的优先级比lock要低,获得pin之前必须先获得lock。同样按照前面所说的例子,当第二次使用varchar2类型的绑定变量执行相同的SQL语句时,该进程首先会获得bucket 64367的句柄上的lock,根据该句柄里所记录的heap发现不能共用时,到shared pool中分配可用的chunk作为heap(包括heap 0和heap 6等)的空间,并获得该heap上的pin,然后在句柄里添加一条子游标记录,以指向所分配的heap 0的句柄。当pin住了heap以后,进程就向heap中写入数据,结束以后释放pin,最后释放lock。当某个进程获得了句柄上的lock,但是不能pin住该句柄所对应的heap时,该进程就必须等待(体现的等待事件就是library cach pin)。与lock相同,当进程试图pin住某个heap但是发现该heap不存在时,就会同时将该heap加载到library cache里同时pin住它。
pin有两种模式:share和exclusive。当某个进程只需要读取heap中的信息时,会对该heap执行share模式的pin。如果进程需要修改heap时,则会先对该heap执行share模式的pin以便对heap进行错误和安全检查,通过以后,再对该heap执行exclusive模式的pin,从而对该heap进行修改。
从上面对lock和pin的描述中可以看出,lock本身不是一个原子的操作,也就是说要完成lock需要执行一系列的操作步骤(包括pin住heap等)。因此为了防止lock的过程被其他进程打破,oracle使用library cache latch来管理lock。也就是说,如果某个进程在进行lock之前,必须先获得library cache latch,如果不能获得该latch,就必须等待。当lock过程结束以后,释放该latch。
oracle提供了多个library cache latch(这样,每个library cache latch都称为子latch)来保护library cache中的bucket。这些子latch的数量由一个隐藏参数决定:_kgl_latch_count。该参数缺省值为大于等于系统中CPU个数的最小的素数。比如在一个具有4个CPU的生产环境中,library cache latch的个数为5,如下所示。但是oracle内部(9i版本)规定了library cache latch的最大个数为67,即便将这个隐藏参数设置为100,library cache latch的数量也还是67个。
SQL> select x.ksppinm, y.ksppstvl, x.ksppdesc
2 from x$ksppi x , x$ksppcv y
3 where x.indx = y.indx
4 and x.ksppinm like '\_%' escape '\'
5 and ksppinm like '%kgl_latch_count%'
6 ;
KSPPINM KSPPSTVL KSPPDESC
-------------------- ---------- ----------------------------------------
_kgl_latch_count 5 number of library cache latches
具体到每个bucket应该由哪个子latch来管理,则是通过下面这个函数来确定的。
latch号=mod(bucket号,latch的数量)
假如还是按照上面的例子,对于bucket 64367来说,假设当前系统具有37个library cache latch,那么会使用24(mod(64367,37)=24)号latch来保护挂在该bucket上的句柄。正是由于这样的算法,可能会导致所有的子latch不能在library cache里的整个bucket链条上均匀分布,有可能出现某个或某几个子latch非常繁忙,而有些子latch则非常空闲。至于如何判断以及解决,可以见下面shared pool的优化部分。
我们来做两个测试,分别来模拟一下lock和pin。来看看lock和pin是如何控制library cache里的对象的。试验的思路很简单,第一,打开一个session(sess #1)创建一个存储过程,该过程只做一件事情,就是通过调用dbms_lock.sleep进行等待。并在sess #1中调用该存储过程;第二,打开第二个session(sess #2),重新编译该存储过程;第三,打开第三个session(sess #3),删除该存储过程;第四,打开第四个session(sess #4)进行监控。根据前面对lock和pin的描述,我们可以预见,sess #2将等待library cache pin。而sess #3会等待library cache lock。
试验过程如下,在试验的过程中,不断以level 16转储library cache以更深入的观察lock和pin的变化,以下按照时间顺序排列:
sess #1
SQL> create or replace procedure lock_test
2 is
3 begin
4 sys.dbms_lock.sleep(5000);
5 end;
6 /
SQL> exec lock_test;
sess #4,转储出来的文件编号为F1。
SQL> ALTER SESSION SET EVENTS 'immediate trace name library_cache level 16';
sess #2
SQL> select sid from v$mystat where rownum=1;
SID
----------
9
SQL> alter procedure lock_test compile; --这时该命令停住了。
sess #4,转储出来的文件编号为F2。
SQL> ALTER SESSION SET EVENTS 'immediate trace name library_cache level 16';
sess #3
SQL> select sid from v$mystat where rownum=1;
SID
----------
10
SQL> drop procedure lock_test; --这时该命令也停住了
sess #4,转储出来的文件编号为F3。
SQL> ALTER SESSION SET EVENTS 'immediate trace name library_cache level 16';
SQL> select sid,event from v$session_wait where sid in(9,10);
SID EVENT
---------- ----------------------------------------------------------------
9 library cache pin
10 library cache lock
从监控的结果看到,正如我们所预料的,编译存储过程的sess #2(sid为9)正在等待library cache pin,而删除存储过程的sess #3(sid为10)正在等待library cache lock。在转储出来的文件中,我们主要关存储过程lock_test本身在library cache中的变化。在F1中,我们可以看到如下图十的内容。注意其中的lock为N,pin为S。由于sess #1正在执行lock_test存储过程,需要读取该handle所对应的heap里的内容,因此以null模式lock住句柄,同时以share模式pin住了heap。这时该对象句柄可以被其他进程以任何模式锁定,但是该句柄对应的heap只能被其他进程以share模式pin,而不能以exclusive模式pin。

图十
我们打开发出编译命令以后生成的F2,找到与图十同样的部分,如下图十一所示。由于sess #2会对存储过程lock_test进行编译,因此需要重新刷新该对象的heap中的信息。所以需要以exclusive模式lock住该对象的句柄,同时以exclusive模式pin住该对象的heap。这时,由于当前句柄上存在null模式的lock,因此sess #2申请exclusive的lock能够成功,但是由于当前该句柄对应的heap上已经存在share的pin,因此申请exclusive的pin时,必须等待,这时体现为sess #2等待library cache pin。

图十一
这时,我们发出删除命令以后,很明显的,要删除存储过程lock_test,sess #3必须以exclusive模式获得lock_test句柄的lock。而这时该句柄上已经存在了exclusive模式的lock,于是这时sess #3只有等待sess #2所添加的exclusive模式的lock释放以后才能继续进行。体现在v$session_wait等相关视图里就是等待library cache lock。这时,我们甚至可以发现,整个“drop procedure lock_test”命令都没有出现在library cache里。也就是说,oracle已经为该SQL语句分配了chunk,但是由于无法获得所引用对象的lock,从而使得所分配的chunk还没有能够挂到bucket上去,也就还没有进入library cache里。
至于如何诊断以及解决这两个等待事件的话,可以见下面shared pool的优化部分。
(待续......)