对于lock这个词,要从两方面理解,首先,它代表的是一种控制机制;其次,在这个机制中有个一成员也叫lock。
从机制的角度看,lock框架包括3个组件:resource structure(资源);lock structure(锁);enqueue(排队机制)
resource和lock是数据结构,enqueue是使用的算法。
下面看一下lock的组成结构图:
resource structure:oracle对于每个需要进行“并发控制”的资源,都在SGA中用一个数据结构来描述它,这个数据结构就叫做resource structure。这个数据结构中有3个与“并发控制” 有关的成员:Owner ,Waiter , Converter这是三个指针,分别指向3个由lock structrue组成的链表。
lock structure:每当进程要访问共享资源时,必须先锁定该资源,这个动作实际就是从内存中申请一个lock sturture 在其中记录“锁模式,进程ID”等重要信息,然后看是否能够立即获得资源的访问权,如果不能,则把这个lock structure 挂到resource structure 的waiter 链表中;
如果能够获得,则把lock structure 挂到resource structure的owene链表中。上面说的resource structure 中的owner ,waiter converter 三个成员就是指向由lock struncture组成的链表指针。
enqueue算法:lock使用的是enqueue算法,可以理解为“先入先出队列”。如果进程的锁定请求不能满足,该进程的lock structure 就被加到waiter 链表的末端。当占用进程释放锁时,会检查waiter 和 converter 队列,把锁分配给最先进入队列的请求者。
converter 和 waiter 两个都是等待队列,二者的用法有细微区别:如果两个操作先后需要两种不同模式的锁,比如先是shared mode然后是 exclusive mode,则进程会先请求shared mode ,获得后lock structure 会挂载owner队列上,当需要exclusive mode 锁时,请求进程必须先释放shared mode的锁,然后再次申请exclusive mode 的锁,但是这可能无法立即获得。这时,这个请求就会被挂在converter队列下,converter队列会优先于waiter队列被处理。
可以从v$lock视图中看到这些lock信息,并且还可以根据v$lock视图的lmode 和request mode 判断出谁是owner,waiter 和 converter:
lmode>0 , request=0 ==> owner
lmode=0 , request>0 ==> waiter(acquirer)
lmode>0 , request >0 ==> converter
数据记录的行级锁
在讲到lock的上文中已经提到,这种控制机制需要resource,lock联众数据结构,但是需要内存分配lock数据结构,对于粗粒度(可以理解为文件很大)或者数量有限的资源,使用这种机制还可以接受,因为分配的内存并不多。但是对于表的数据记录,动辄几百G 的表,每个记录如果都分配一个resource 和lock 数据结构对,无论从内存需求还是维护开销上都是一个噩梦。所以,对于数据记录这种细粒度的资源,oracle使用的是行级锁(row level lock)。行级锁的实现原理如下图,左边代表数据块,右边代表回滚段(undo segment):
具体数据块的结构详细介绍参考我之前的博客:http://blog.csdn.net/changyanmanman/article/details/7076935
我们现在只需要知道,数据块内存储的是一条条的用户记录,用户记录也是按照一定的格式保存的,每条记录可以分成 记录头 和 记录体 两部分。记录头中是描述信息,比如列宽度,和事务有关的是ITL Entry pointer字段。整个行级锁供涉及以下4中数据结构,这是我们要研究的重点:
1)ITL:这个比较熟悉,用于记录哪些事务修改了这个数据块的内容,可以把他想象成一个表格,每个表格对应一个事务,包括事务号,事务是否提交等重要信息。
2)记录头ITL索引:每条记录的记录头部有一个字段,用于记录ITL表项号,可以看做是指向ITL表的指针,如果一个进程来访问记录,那就先得根据这个指针去ITL中看看事务已经提交了没啊。。前一个事务是用的什么锁啊。。等待
3)TX锁:这个锁代表一个事务,这个锁属于上文讲到的lock机制,有resource structure , lock structure , enqueue 算法。
4)TM锁:这个锁也属于前面讲到的lock机制,用于保护对象(表,视图等)的定义不被修改。
下面我们详细说一下运行机制:
当一个事务开始时,必须先申请一个TX锁,注意 这种锁保护的资源是回滚段,回滚数据块。因此这个申请也就意味着:用户必须先申请到一个回滚段资源后才能开始一个事务,才能执行语句修改数据。
申请到回滚段资源后,用户事务就可以修改数据了。在修改数据表的记录时,需要遵循下面的操作顺序:
1、首先获得这个表的TM锁,这个锁用于保护事务执行过程中其他用户不能修改表结构(但是可以修改表内的数据)。
2、事务修改某个数据块中的记录时,首先要在数据块块头的ITL表中申请一个空闲表项,并且在其中记录事务号,实际就是记录这个事务要使用的回滚段地址。
3、事务修改该数块中的某条记录时,会设置该记录头部的ITL索引指向上一步申请到的表项。然后再修改记录的内容,修改前现在回滚段对记录修改该钱的状态做一个拷贝,然后才能修改该数据记录,这个拷贝用于以后的回滚,恢复,或者一致性读。
4、当其他用户并发修改该这条记录时,会根据记录头的ITL索引读取ITL表项内容。查看这个事务石头已经提交。
5、如果没有提交,则这个用户的TX锁会等待前一个用户的TX锁的释放。
从上面的工作机制可以看出,无论一个事务修改多少个表的多少条记录,盖世五真正需要的只是一个TX锁,每个表一个的TM锁,内存开销非常小。而所谓的行级锁,其实只是数据块头,数据记录的一些字段,不会消耗额外的内存资源。
锁模式:
在介绍锁模式之前,我们先要了解一个概念,oracle中的对象分类:oracle中的对象分为两种:简单对象(simple object) 和 复合对象(compound object) 数据表是典型的复合对象,表中的每条记录都是典型的简单对象。
对于简单对象,只有三种锁模式:null ,share , exclusive
对于符合对象,除了这种外,还有三种 :sub-shared,sub-exclusive,share-sub-exclusive
之所以吧对象划分成联众类型,也是从性能角度考虑。比如oracle提供lock table语句允许对表加锁,在锁表之前,oracle必须确保没有其他用户操作表中的数据 。加入之后行级锁,就会有这样一个问题,oracle必须对表中的所有记录做一个遍历,才能确定是否有不兼容的锁模式存在,而且还得对扫描过的记录加锁,以阻止其他用户操作,对系统并发能力影响很大。通过吧对象按简单对象,复合对象分类,并使用不同锁,对想能有很大提升。
下图列举了常见sql语句需要的锁模式已经模式键的兼容关系,其中第一列:sql操作,第二列:(mode of tbale lock)指的是左边也就是第一列语句加在表上的锁模式。 第三列:(lock modes permitted)以及下面的5个小列,分别说明不同模式间的兼容关系。
下面我们通过几个语句来学习这个表:
select * from a;
对照表的第一行,这个语句不会在表a上添加任何模式的锁,兼容其他所有模式的锁。也就是说用户a在执行select操作时,其他用户可以执行update,insert,delete,lock table等所有操作。
insert a values (...) / update/delete
看表的第2到4行,由第二列可见这3个语句都会在表a上添加RX模式的锁(sub-exclusive)这个模式的锁能兼容RS,还有自己同样的RX锁,从该表的第二列还可以看到insert、update、delete 这3个语句是RX模式的锁,而select ....for update . lock table ...in row share mode 是RS模式的锁,也就是说当执行 insert 、update 、delete 语句时,其他用户只能执行 上述5个操作,而不能执行其他操作。
但是这里所说的允许操作,只是只在表级别(或者说复合对象级别)允许操作,单最终操作是否能成功还要根据行级锁。这也是update、delete、select ...for update 语句后面 Y* 加星号的原因。
下面我们以library cache 为例,看一下null 锁模式,我们知道这个library cache 中存放的是 所有的sql语句,执行计划,包 等对象,以及这些对象所引用的对象(比如执行计划引用的对象表) 当一个语句或包编译时,这个语句引用的对象都会加上一个 library cache lock ,而执行时,所有这些引用对象都要背加一个library cache pin。 以保证在语句执行过程中,应用对象的结构不会被修改。
编译完成后,加在引用对象上的library cache lock 会由原来的share 或者 exclusive模式 变成 null 模式,null模式的library cache lock 就相当于一个触发器,当这些对象的定义被修改时,引用他的对象将被置为无效,必须重新编译。
我们举个例子:比如:“select * from a ”编译之后,这个语句的执行计划对象会在a上加一个null模式的library cache lock,这个null模式的lock相当于一个触发器,以后如果对表a的结构做了修改,比如增加一个字段,这个触发器就会导致“select * from a ”这个语句的执行计划失效。如果再次执行这个语句,旧的执行计划就不能再被使用,必须重新编译产生新的执行计划。
在RAC环境中,要知道,每个节点都可能有表a的引用对象,无论在任何一个节点上对a的结构进行修改,都需要把所有节点上应用a的对象(即各个节点library cache中执行计划)值为无效,因此除了传统的library cache lock之外,每个节点上的LCK0进程,会对本实例library cache中的对象加一个共享模式(shared-mode)的IV(Invalidation 无效) Instance Lock,如果某个用户想要修改对象定义,必须获得一个exclusive模式的iv锁,这会通知本地的LCK0释放shared-mode锁,本地LCK0在释放这个锁之前,会通知其他节点上的LCK0,其他节点的LCK0收到这个消息后,就会将本地library cache中 的所有相关对象值为失效,这种机制是一种广播机制,这些通信过程是通过实例的LMD进程完成的。
对于cache fusion 资源。比如典型额buffer cache 的数据块数量更多,修改更加密集,如果每次数据块的修改都在集群内发送广播,这种方式显得太低效了。因此,对于数据块这种资源,oracle采用的是cache fusion机制,这种机制使用的是pcm lock。关于pcm lock 。。。。它有三种模式shared ,exclusive,null。。对于SGA中的数据块,oracle用另一个属于来描述加在每个数据块上的锁类型——数据块状态(buffer state)