6.3 锁类型
Oracle中主要有3类锁,具体是:
l DML锁(DML lock):DML代表数据操纵语言(Data Manipulation Language) 。一般来讲,这表示SELECT、INSERT、UPDATE、MERGE 和 DELETE 语句。DML 锁机制允许并发执行数据修改。例如,DML 锁可能是特定数据行上的锁,或者是锁定表中所有行的表级锁。
l DDL锁(DDL lock):DDL代表数据定义语言(Data Definition Language),如CREATE和ALTER语句等。DDL锁可以保护对象结构定义。
l 内部锁和闩:Oracle使用这些锁来保护其内部数据结构。例如,Oracle解析一个查询并生成优化的查询计划时,它会把库缓存“临时闩”,将计划放在那里,以供其他会话使用。闩(latch)是Oracle采用的一种轻量级的低级串行化设备,功能上类似于锁。不要被“轻量级”这个词搞糊涂或蒙骗了,你会看到,闩是数据库中导致竞争的一个常见原因。轻量级指的是闩的实现,而不是闩的作用。
6.3.1 DML锁
1. TX 锁(事务锁)
事务发起第一个修改时会得到TX锁(事务锁) ,而且会一直持有这个锁,直至事务执行提交(COMMIT)或回滚(ROLLBACK)。TX锁用作一种排队机制,使得其他会话可以等待这个事务执行。事务中修改或通过SELECT FOR UPDATE选择的每一行都会“指向”该事务的一个相关 TX 锁。在Oracle中,闩为数据的一个属性(第10章会给出Oracle块格式的一个概述)
Oracle中的锁定过程如下:
(1) 找到想锁定的那一行的地址。
(2) 到达那一行。
(3) 锁定这一行(如果这一行已经锁定,则等待锁住它的事务结束,除非使用了NOWAIT选项)。
仅此而已。由于闩为数据的一个属性,Oracle不需要传统的锁管理器。事务只是找到数据[5],如果数据还没有被锁定,则对其锁定。有意思的是,找到数据时,它可能看上去被锁住了,但实际上并非如此。在 Oracle 中对数据行锁定时,行指向事务 ID 的一个副本,事务ID存储在包含数据的块中,释放锁时,事务ID却会保留下来。这个事务ID是事务所独有的,表示了回滚段号、槽和序列号。事务ID留在包含数据行的块上,可以告诉其他会话:你“拥有”这个数据(并非块上的所有数据都是你的,只是你修改的那一行“归你所有”)。另一个会话到来时,它会看到锁ID,由于锁ID表示一个事务,所以可以很快地查看持有这个锁的事务是否还是活动的。如果锁不活动,则允许会话访问这个数据。如果锁还是活动的,会话就会要求一旦释放锁就得到通知。因此,这就有了一个排队机制:请求锁的会话会排队,等待目前拥有锁的事务执行,然后得到数据。
举例说明一个事务阻塞另一个事务时以下三个视图可以显示出来:
l V$TRANSACTION,对应每个活动事务都包含一个条目。
l V$SESSION,显示已经登录的会话。
l V$LOCK,对应持有所有enqueue队列锁以及正在等待锁的会话,都分别包含一个条目。这并不是说,对于表中被会话锁定的每一行,这个视图中就有相应的一行。你不会看到这种情况。如前所述,不存在行级锁的一个主列表。如果某个会话将EMP表中的一行锁定,V$LOCK视图中就有对应这个会话的一行来指示这一事实。如果一个会话锁定了EMP表中的数百万行,V$LOCK视图中对应这个会话还是只有一行。这个视图显示了各个会话有哪些队列锁。
为还有最后一个主题需要说明:如何用数据本身来管理锁定和事务信息。这是块开销的一部分。在第9章中,我们会详细分析块的格式,但是现在只需知道数据库块的最前面有一个“开销”空间,这里会存放该块的一个事务表,了解这一点就足够了。对于锁定了该块中某些数据的各个“实际”事务,在这个事务表中都有一个相应的条目。这个结构的大小由创建对象时CREATE语句上的两个物理属性参数决定:
l INITRANS:这个结构初始的预分配大小。对于索引和表,这个大小默认为2(不过我已经提出,Oracle SQL Reference手册中与此有关的说明有问题)。
l MAXTRANS:这个结构可以扩缩到的最大大小。它默认为255,在实际中,最小值为2。在Oracle 10g中,这个设置已经废弃了,所以不再使用。这个版本中的MAXTRANS总是255。在Oracle 10g中, 只要块上的空间允许, 即使设置了MAXTRANS, Oracle也会不受约束地扩大事务表。
默认情况下,每个块最开始都有两个事务槽。一个块上同时的活动事务数受 MAXTRANS 值的约束,另外也受块上空间可用性的限制。如果没有足够的空间来扩大这个结构,块上就无法得到255个并发事务。
2. TM (DML Enqueue)锁
TM锁(TM lock)用于确保在修改表的内容时,表的结构不会改变。
尽管每个事务只能得到一个TX锁,但是TM锁则不同,修改了多少个对象,就能得到多少个TM锁。在此,有意思的是,TM锁的ID1列就是DML锁定对象的对象ID,所以,很容易发现哪个对象持有这个锁。
系统中允许的TM锁总数可以由DML_LOCKS 参数定义。实际上,这个数可能设置为 0。但这并不是说你的数据库变成了一个只读数据库(没有锁),而是说不允许DDL。在非常专业的应用(如RAC实现) 中, 这一点就很有用,可以减少实例内可能发生的协调次数。 通过使用ALTER TABLE TABLENAME DISABLE TABLE LOCK命令,还可以逐对象地禁用TM锁。这是一种快捷方法,可以使意外删除表的“难度更大”,因为在删除表之前,你必须重新启用表锁。还能用它来检测由于外键未加索引而导致的全表锁(前面已经讨论过)。
6.3.2 DDL锁
在DDL操作中会自动为对象加DDL锁(DDL Lock),从而保护这些对象不会被其他会话所修改。实际上,通常会把 DDL 语句包装在隐式提交(或提交/回滚对)中来执行这些工作。由于这个原因,在Oracle中DDL一定会提交(即使提交不成功也会如此)。每条CREATE、ALTER等语句实际上都如下执行(这里用伪代码来展示):
Begin
Commit;
DDL-STATEMENT
Commit;
Exception
When others then rollback;
End;
因此,DDL 总会提交(即使提交不成功也会如此)。DDL 一开始就提交,一定要知道这一点。它首先提交,因此如果必须回滚,它不会回滚你的事务。如果你执行了DDL,它会使你所执行的所有未执行的工作成为永久性的,即使DDL不成功也会如此。如果你需要执行DDL,但是不想让它提交你现有的事务,就可以使用一个自治事务(autonomous transaction)。
有3种类型的DDL锁:
l 排他DDL锁(Exclusive DDL lock) :这会防止其他会话得到它们自己的DDL锁或TM(DML)锁。这说明,在DDL操作期间你可以查询一个表,但是无法以任何方式修改这个表。
l 共享DDL锁(Share DDL lock) :这些锁会保护所引用对象的结构,使之不会被其他会话修改,但是允许修改数据。
l 可中断解析锁(Breakable parse locks) :这些锁允许一个对象(如共享池中缓存的一个查询计划)向另外某个对象注册其依赖性。如果在被依赖的对象上执行DDL,Oracle会查看已经对该对象注册了依赖性的对象列表,并使这些对象无效。因此,这些锁是“可中断的”,它们不能防止DDL出现。
对于可中断解析锁。 你的会话解析一条语句时, 对于该语句引用的每一个对象都会加一个解析锁。 加这些锁的目的是:如果以某种方式删除或修改了一个被引用的对象,可以将共享池中已解析的缓存语句置为无效(刷新输出)。
有一个意义非凡的视图可用于查看这个信息,即 DBA_DDL_LOCKS 视图。对此没有相应的 V$视图。DBA_DDL_LOCKS 视图建立在更神秘的X$表基础上,而且默认情况下, 你的数据库上不会安装这个视图。 可以运行[ORACLE_HOME]/rdbms/admin目录下的catblock.sql脚本来安装这个视图以及其他锁视图。必须作为用户SYS来执行这个脚本才能成功。这个视图对开发人员很有用,发现测试或开发系统中某段代码无法编译时,将会挂起并最终超时。这说明,有人正在使用这段代码(实际上在运行这段代码),你可以使用这个视图来查看这个人是谁。对于 GRANTS 和对象的其他类型的 DDL 也是一样。例如,无法对正在运行的过程授予EXECUTE权限。可以使用同样的方法来发现潜在的阻塞者和等待者。
6.3.3 闩
闩(latch)是轻量级的串行化设备,用于协调对共享数据结构、对象和文件的多用户访问。 闩就是一种锁,设计为只保持极短的一段时间(例如,修改一个内存中数据结构所需的时间)。闩用于保护某些内存结构,如数据库块缓冲区缓存或共享池中的库缓存。一般会在内部以一种“愿意等待”(willing to wait)模式请求闩。这说明,如果闩不可用,请求会话会睡眠很短的一段时间,并在以后再次尝试这个操作。还可以采用一种“立即”(immediate)模式请求其他闩,这与 SELECT FOR UPDATE NOWAIT 的思想很相似,说明这个进程会做其他事情(如获取另一个与之相当的空闲闩) ,而不只是坐而等待这个闩直到它可用。由于许多请求者可能会同时等待一个闩,你会看到一些进程等待的时间比其他进程要长一些。Oracle使用诸如“测试和设置”(test and set)以及“比较和交换”(compare and swap)之类的原子指令来处理闩。由于设置和释放闩的指令是原子性的,尽管可能有多个进程在同时请求它,但操作系统本身可以保证只有一个进程能测试和设置闩。
等待闩可能是一个代价很高的操作。如果闩不是立即可用的,我们就得等待(大多数情况下都是如此) ,在一台多 CPU 机器上,我们的会话就会自旋(spin) ,也就是说,在循环中反复地尝试来得到闩。出现自旋的原因是,上下文切换(context switching)的开销很大(上下文切换是指被“踢出”CPU,然后又必须调度回CPU)。
2. 测量闩定共享资源的开销
举个例子,我们来研究闩定共享池的开销。我们会把一个编写得很好的程序和一个编写得不太好的程序进行比较,前者使用了绑定变量,而在编写得不好的程序中,每条语句使用了SQL直接量或各不相同的 SQL。这两个方式的开销相差很大,所以在写SQL语句时一定要使用绑定变量的方式。