oracle 锁

Oracle中主要有3类锁:

1. DML锁 数据操纵语言(Data Manipulation Language),SELECT、INSERT、UPDATE、MERGE和DELETE语句。

DML锁机制允许并发执行数据修改。例如,DML锁可能是特定数据行上的锁,或者是锁定表中所有行的表级锁。

2. DDL锁 数据定义语言(Data Definition Language),如CREATE和ALTER语句等。

DDL锁可以保护对象结构定义。

3. 内部锁和闩 保护Oracle内部数据结构。例如,Oracle解析一个查询并生成优化的查询计划时,它会把库缓存“临时闩”,将计划放在那里,以供其他会话使用。

闩(latch)是Oracle采用的一种轻量级的低级串行化设备,功能上类似于锁。闩是数据库中导致竞争的一个常见原因。

轻量级指的是闩的实现,而不是闩的作用。



DML锁

用于确保一次只有一个人能修改某一行,而且你正在处理一个表时别人不能删除这个表。

1. TX锁(事务锁)

事务发起第一个修改时会得到TX锁,而且会一直持有这个锁,直至事务执行提交(COMMIT)或回滚(ROLLBACK)。

TX锁用作一种排队机制,使得其他会话可以等待这个事务执行。事务中修改或通过SELECT FOR UPDATE选择的每一行都会“指向”该事务的一个相关TX锁。

在Oracle中,闩为数据的一个属性。Oracle并没有一个传统的锁管理器,不会为系统中锁定的每一行维护一个长长的列表。

如果数据库中有一个传统的基于内存的锁管理器,在这样一个数据库中,对一行锁定的过程一般如下:
(1) 找到想锁定的那一行的地址。
(2) 在锁管理器中排队(锁管理器必须是串行化的,因为这是一个常见的内存中的结构)。
(3) 锁定列表。
(4) 搜索列表,查看别人是否已经锁定了这一行。
(5) 在列表中创建一个新的条目,表明你已经锁定了这一行。
(6) 对列表解锁。
既然已经锁定了这一行,接下来就可以修改它了。之后,在你提交修改时,必须继续这个过程,如下:
(7) 再次排队。
(8) 锁住锁的列表。
(9) 在这个列表中搜索,并释放所有的锁。
(10) 对列表解锁。

Oracle中的锁定过程如下:
(1) 找到想锁定的那一行的地址。
(2) 到达那一行。
(3) 锁定这一行(如果这一行已经锁定,则等待锁住它的事务结束,除非使用了NOWAIT选项)。


事务只是找到数据,如果数据还没有被锁定,则对其锁定。

在Oracle中对数据行锁定时,行指向事务ID的一个副本,事务ID存储在包含数据的块中,释放锁时,事务ID却会保留下来。

扫描二维码关注公众号,回复: 184307 查看本文章

这个事务ID是事务所独有的,表示了回滚段号、槽和序列号。

事务ID留在包含数据行的块上,可以告诉其他会话:“拥有”这个数据(并非块上的所有数据,只是修改的那一行)。

另一个会话到来时,它会看到锁ID,由于锁ID表示一个事务,所以可以很快地查看持有这个锁的事务是否还是活动的。

如果锁不活动,则允许会话访问这个数据。如果锁还是活动的,会话就会要求一旦释放锁就得到通知。

因此,这就有了一个排队机制: 请求锁的会话会排队,等待目前拥有锁的事务执行,然后得到数据

以下是一个小例子,展示了这到底是怎么回事,这里使用了3个V$ 表:
1. V$TRANSACTION,对应每个活动事务都包含一个条目。
2. V$SESSION,显示已经登录的会话。
3. V$LOCK,对应持有所有enqueue队列锁以及正在等待锁的会话,都分别包含一个条目。不存在行级锁的一个主列表。如果某个会话将EMP表中的一行锁定,V$LOCK视图中就有对应这个会话的一行来指示这一事实。如果一个会话锁定了EMP表中的数百万行,V$LOCK视图中对应这个会话还是只有一行。这个视图显示了各个会话有哪些队列锁。
scott@ORCL>create table dept_bak as select * from dept;
表已创建。

scott@ORCL>update dept_bak set deptno = deptno+10;
已更新4行。

下面来看看此时系统的状态:
scott@ORCL>select username,
  2  v$lock.sid,
  3  trunc(id1/power(2,16)) rbs,
  4  bitand(id1,to_number('ffff','xxxx'))+0 slot,
  5  id2 seq,
  6  lmode,
  7  request
  8  from v$lock, v$session
  9  where v$lock.type = 'TX'
 10  and v$lock.sid = v$session.sid
 11  and v$session.username = USER;


USERNAME                              SID        RBS       SLOT        SEQ	LMODE    REQUEST
------------------------------ ---------- ---------- ---------- ---------- ---------- ----------
SCOTT                                  68          6          0       2146	6          0


scott@ORCL>select XIDUSN, XIDSLOT, XIDSQN
  2  from v$transaction;


    XIDUSN    XIDSLOT     XIDSQN
---------- ---------- ----------
         6          0       2146
1. $LOCK表中的LMODE为6,REQUEST为0。 LMODE=6是一个排他锁请求(REQUEST)值为0则意味着没有发出请求;也就是说,你拥有这个锁
2. 这个表中只有一行。V$LOCK表更应算是一个队列表而不是一个锁表。Oracle不会在任何地方存储行级锁的列表(不会为每一个被锁定的行维护一个主列表)。要查看某一行是否被锁定,必须直接找到这一行。
3. 选择 ID1和ID2列,并对它们执行了一些处理。Oracle需要保存3个16位的数,但是对此只有两个列。所以,第一个列ID1保存着其中两个数。通过用trunc(id1/power (2,16))rbs除以2^16,并用bitand(id1,to_number('ffff','xxxx'))+0 slot把高位屏蔽,就能从这个数中找回隐藏的两个数。
4. RBS、SLOT和SEQ值与V$TRANSACTION信息匹配。这就是我的事务ID。


下面使用scott启动另一个会话,更新EMP_bak中的某些行,并希望试图更新DEPT_bak:
scott@ORCL>update emp_bak set ename = lower(ename);
已更新15行。

scott@ORCL>update dept_bak set deptno = deptno-10;

现在会话2会阻塞。如果会话1 再次运行V$查询,可以看到下面的结果:
scott@ORCL>select username,
  2  v$lock.sid,
  3  trunc(id1/power(2,16)) rbs,
  4  bitand(id1,to_number('ffff','xxxx'))+0 slot,
  5  id2 seq,
  6  lmode,
  7  request
  8  from v$lock, v$session
  9  where v$lock.type = 'TX'
 10  and v$lock.sid = v$session.sid
 11  and v$session.username = USER;

USERNAME                              SID        RBS       SLOT        SEQ	LMODE    REQUEST
------------------------------ ---------- ---------- ---------- ----------	---------- ----------
SCOTT                                 133          6          0       2146	0          6
SCOTT                                  68          6          0       2146	6          0
SCOTT                                 133          8         20       2330	6          0

scott@ORCL>select XIDUSN, XIDSLOT, XIDSQN
  2  from v$transaction;

    XIDUSN    XIDSLOT     XIDSQN
---------- ---------- ----------
         6          0       2146
         8         20       2330
可以看到一个新的事务(8,20,2330)
这一次,这个新会话(SID=133)在V$LOCK中有两行。其中一行表示它所拥有的锁(LMODE=6)。
另外一行,显示了一个 REQUEST=6,这是一个对排他锁的请求。这个请求行的RBS/SLOT/SEQ值正是锁持有者的事务ID。SID=68的事务阻塞了SID=133的事务。
,
只需执行V$LOCK的一个自联结,就可以更明确地看出这一点:
scott@ORCL>select
  2     (select username from v$session where sid=a.sid) blocker,
  3     a.sid,
  4     ' is blocking ',
  5     (select username from v$session where sid=b.sid) blockee,
  6     b.sid
  7  from v$lock a, v$lock b
  8  where a.block = 1
  9     and b.request > 0
 10     and a.id1 = b.id1
 11     and a.id2 = b.id2;


BLOCKER                               SID 'ISBLOCKING'  BLOCKEE		SID
------------------------------ ---------- ------------- ----------	------
SCOTT                                  68  is blocking  SCOTT		133
现在,如果提交原来的事务(SID=68),并重新运行锁查询,可以看到请求行不见了:
scott@ORCL>commit;
提交完成。

scott@ORCL>select username,
  2     v$lock.sid,
  3     trunc(id1/power(2,16)) rbs,
  4     bitand(id1,to_number('ffff','xxxx'))+0 slot,
  5     id2 seq,
  6     lmode,
  7     request
  8  from v$lock, v$session
  9  where v$lock.type = 'TX'
 10     and v$lock.sid = v$session.sid
 11     and v$session.username = USER;


USERNAME                              SID        RBS       SLOT        SEQ		LMODE    REQUEST
------------------------------ ---------- ---------- ---------- ----------		----- ----------
SCOTT                                 133          8         20       2330		6          0


scott@ORCL>select XIDUSN, XIDSLOT, XIDSQN
  2  from v$transaction;


    XIDUSN    XIDSLOT     XIDSQN
---------- ---------- ----------
         8         20       2330
另一个会话一旦放弃锁,请求行就会消失。这个请求行就是排队机制。一旦事务执行,数据库会唤醒被阻塞的会话。

如何用数据本身来管理锁定和事务信息。这是块开销的一部分。数据库块的最前面有一个“开销”空间,这里会存放该块的一个事务表,
对于锁定了该块中某些数据的各个“实际”事务,在这个事务表中都有一个相应的条目。这个结构的大小由创建对象时CREATE语句上的两个物理属性参数决定:
1. INITRANS:这个结构初始的预分配大小。对于索引和表,这个大小默认为2
2. MAXTRANS:这个结构可以扩缩到的最大大小。它默认为255


默认情况下,每个块最开始都有两个事务槽。
一个块上同时的活动事务数受MAXTRANS值的约束,另外也受块上空间可用性的限制。如果没有足够的空间来扩大这个结构,块上就无法得到255个并发事务。
我们可以创建一个具有受限MAXTRANS的表,来专门展示这是如何工作的。在Oracle9i 及以前的版本中,一旦块达到了MAXTRANS值,事务表就不会再扩大了,例如:           
scott@ORCL>create table t ( x int ) maxtrans 2;
表已创建。

scott@ORCL>insert into t select rownum from all_users;
已创建38行。

scott@ORCL>commit;
提交完成。

scott@ORCL>select distinct dbms_rowid.rowid_block_number(rowid) from t;
DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID)
------------------------------------
                                3612     
因此,我们有24行,而且经验证,它们都在同一个数据库块上。现在,在一个会话中发出以下命令:  
scott@ORCL>update t set x = 1 where x = 1;
已更新 1 行。

在会话2中,发出下面的命令:
scott@ORCL>update t set x = 2 where x = 2;
已更新 1 行。

最后,在会话3中,发出如下命令:   
scott@ORCL>update t set x = 3 where x = 3;
现在,由于这3行在同一个数据库块上,而且我们将MAXTRANS(该块的最大并发度)设置为2,所以第3个会话会被阻塞。  

注意 在Oracle 10g 及以上版本,不会发生上例中出现的阻塞,不管怎样,MAXTRANS都会设置为255。只有当块上没有足够的空间来扩大事务表时,才会看到这种阻塞。

如果多个MAXTRANS事务试图同时访问同一个块时会出现阻塞。类似地,如果INITRANS设置得很低,而且块上没有足够的空间来动态地扩缩事务,也会出现阻塞。
大多数情况下,INITRANS的默认值2就足够了,因为事务表会动态扩大(只要空间允许)。但是在某些环境中,可能需要加大这个设置来提高并发性,并减少等待。
比如,在频繁修改的表上就可能要增加INITRANS设置,或者,对于频繁修改的索引也可能需要这么做,因为索引块中的行一般比表中的行多。
你可能需要增加PCTFREE或INITRANS,从而在块上提前预留足够的空间以应付可能的并发事务数。尤其是,如果你预料到块开始时几乎是满的(这说明块上没有空间来动态扩缩事务结构),则更需要增加PCTFREE或INITRANS。     


2. TM (DML Enqueue)锁

用于确保在修改表的内容时,表的结构不会改变。例如,如果你已经更新了一个表,会得到这个表的一个TM锁。这会防止另一个用户在该表上执行DROP或ALTER命令。如果你有表的一个TM锁,而另一位用户试图在这个表上执行DDL,他就会得到以下错误消息:
scott@ORCL>drop table t;
drop table t
           *
第 1 行出现错误:
ORA-00054: 资源正忙, 但指定以 NOWAIT 方式获取资源, 或者超时失效
如果你要执行的操作将要阻塞,但是这个操作不允许阻塞,总是会得到这样一条一般性的消息。
如果在一个锁定的行上发出SELECT FOR UPDATE NOWAIT命令,也会得到同样的消息。

以下显示了这些锁在V$LOCK表中是什么样子:
scott@ORCL>update t set x = 1 where x = 1;
已更新 1 行。

scott@ORCL>update t set x = 1 where x = 1;
已更新 1 行。

scott@ORCL>commit;
提交完成。

scott@ORCL>update t set x = 1 where x = 1;
已更新 1 行。

scott@ORCL>create table t1 ( x int );
表已创建。

scott@ORCL>create table t2 ( x int );
表已创建。

scott@ORCL>insert into t1 values ( 1 );
已创建 1 行。

scott@ORCL>insert into t2 values ( 1 );
已创建 1 行。


scott@ORCL>select (select username
  2     from v$session
  3     where sid = v$lock.sid) username,
  4     sid,
  5     id1,
  6     id2,
  7     lmode,
  8     request, block, v$lock.type
  9  from v$lock
 10  where sid = (select sid
 11     from v$mystat
 12     where rownum=1)
 13  /


USERNAME                              SID        ID1        ID2      LMODE    REQUEST      BLOCK TY
------------------------------ ---------- ---------- ---------- ---------- ---------- ---------- --
SCOTT                                  68        100          0          4	0          0 AE
SCOTT                                  68      65921          1          3	0          0 TO
SCOTT                                  68      77102          0          3	0          0 TM
SCOTT                                  68      77103          0          3	0          0 TM
SCOTT                                  68     131090       2131          6	0          0 TX


scott@ORCL>select object_name, object_id
  2  from user_objects
  3  where object_name in ('T1','T2')
  4  /


OBJECT_NAME		OBJECT_ID
------------		----------
T1			77102
T2			77103

每个事务只能得到一个TX锁,但是TM锁则不同,修改了多少个对象,就能得到多少个TM锁。

TM锁的ID1列就是DML锁定对象的对象ID,所以 很容易发现哪个对象持有这个锁。

系统中允许的TM锁总数可以配置。这个数可能设置为0。但这并不是说 数据库变成了一个只读数据库(没有锁),而是说不允许DDL。在非常专业的应用(如RAC实现)中,这一点 可以减少实例内可能发生的协调次数。
通过使用 ALTER TABLE TABLENAME DISABLE TABLE LOCK命令,还可以逐对象地禁用TM锁。
还能用它来检测由于外键未加索引而导致的全表锁。

2. DDL锁

在DDL操作中会自动为对象加DDL锁(DDL Lock),从而保护这些对象不会被其他会话所修改。
例如,如果我执行一个DDL操作ALTERTABLE T,表T上就会加一个排他DDL锁,以防止其他会话得到这个表的DDL锁和TM锁。
在DDL语句执行期间会一直持有DDL锁,一旦操作执行就立即释放DDL锁。
实际上,通常会把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锁:
1. 排他DDL锁(Exclusive DDL lock):这会防止其他会话得到它们自己的DDL锁或TM(DML)锁。在DDL操作期间你可以查询一个表,但是无法以任何方式修改这个表。
2. 共享DDL锁(Share DDL lock):这些锁会保护所引用对象的结构,使之不会被其他会话修改,但是允许修改数据。
3. 可中断解析锁(Breakable parse locks):这些锁允许一个对象(如共享池中缓存的一个查询计划)向另外某个对象注册其依赖性。如果在被依赖的对象上执行DDL,Oracle会查看已经对该对象注册了依赖性的对象列表,并使这些对象无效。因此,这些锁是“可中断的”,它们不能防止DDL出现。

大多数DDL都带有一个排他DDL锁。如果发出如下一条语句:
Alter table t add new_column date;
在执行这条语句时,表T不能被别人修改。在此期间,可以使用SELECT查询这个表,但是大多数其他操作都不允许执行,包括所有DDL语句。
在Oracle中,现在有些DDL操作没有DDL锁也可以发生。例如,可以发出以下语句:
create index t_idx on t(x) ONLINE;

ONLINE关键字会改变具体建立索引的方法。Oracle并不是加一个排他DDL锁来防止数据修改,而只会试图得到表上的一个低级(mode 2)TM锁

防止其他DDL发生,同时还允许DML正常进行

为DDL语句执行期间对表所做的修改维护一个记录,执行CREATE时再把这些修改应用至新的索引

这样能大大增加数据的可用性。



另外一类DDL会获得共享DDL锁。在创建存储的编译对象(如过程和视图)时,会对依赖的对象加这种共享DDL锁。例如:
Create view MyView
as
select emp.*
from emp, dept
where emp.deptno = dept.deptno;
表EMP和DEPT上都会加共享DDL锁,而CREATE VIEW命令仍在处理。可以修改这些表的内容,但是不能修改它们的结构。


最后一类DDL锁是可中断解析锁。会话解析一条语句时,对于该语句引用的每一个对象都会加一个解析锁。
加这些锁的目的是: 如果以某种方式删除或修改了一个被引用的对象,可以将共享池中已解析的缓存语句置为无效(刷新输出)。


DBA_DDL_LOCKS视图 可用于查看这个信息。对此没有相应的V$视图。
DBA_DDL_LOCKS视图建立在X$表基础上,而且默认情况下,数据库上不会安装这个视图。
可以运行 [ORACLE_HOME]/rdbms/admin目录下的 catblock.sql脚本来安装这个视图以及其他锁视图。
必须作为用户SYS来执行这个脚本才能成功。
例如,在一个单用户数据库中,我看到以下结果:
scott@ORCL>select session_id sid, owner, name, type,
  2  mode_held held, mode_requested request
  3  from dba_ddl_locks;


       SID OWNER      	NAME                           	TYPE             	HELD     REQUEST
---------- ------	---------------------------	------------- 		------	---------
       190 SYS		"SYS"."ALERT_QUE":"HAE_SUB"	28			Null      None
       190 SYS          STANDARD			Table/Procedure/Type	Null      None
       190 SYS          DBMS_PRVT_TRACE			Table/Procedure/Type	Null      None
       131 SYS          DBMS_OUTPUT			Body			Null      None
       131 SYS          DBMS_OUTPUT			Table/Procedure/Type	Null      None
       190 SYS          AQ$_ALERT_QT_E			10			Null      None
       131 SYS          DBMS_APPLICATION_INFO		Body			Null      None
       190 SYS          DBMS_HA_ALERTS_PRVT		Table/Procedure/Type	Null      None
       190 SYS          STANDARD			Body			Null      None
         8 SYS          DBMS_LOGSTDBY			Body			Null      None
       190 SYS          ALERT_QUE_R			23			Null      None
       190 SYS          ALERT_QUE_R			23			Null      None
       131 SYS          PLITBLM				Table/Procedure/Type	Null      None
       190 SYS          PLITBLM				Table/Procedure/Type	Null      None
       190 SYS          ALERT_QUE			10			Null      None
       131 SYS          DBMS_APPLICATION_INFO		Table/Procedure/Type	Null      None
       190 SYS          DBMS_HA_ALERTS_PRVT		Body			Null      None
       131 SCOTT        SCOTT				18			Null      None
       131              SCOTT				73			Share     None
       131 SYS          DATABASE			18			Null      None
         8 SYS          DBMS_LOGSTDBY			Table/Procedure/Type	Null      None
       190 SYS          "SYS"."ALERT_QUE":"LOCALHOST_3	28			Null      None
       192 SYS          "SYS"."AQ$_ALERT_QT_E"		28			Null      None
       190 SYS          DBMS_PRVT_TRACE			Body			Null      None
已选择24行。
这些就是我的会话“锁定”的所有对象。我对一组DBMS_*包加了可中断解析锁。这是使用SQL*Plus的副作用;例如,它会调用DBMS_APPLICATION_INFO。
可以看到不同的对象可能有不止一个副本,这是正常的,这只是表明,共享池中有多个“事物”引用了这些对象。在这个视图中,OWNER列不是锁的所有者;而是所锁定对象的所有者。
正是由于这个原因,所以会看到多个SYS行。SYS拥有这些包,但是它们都属于我的会话。


要看到一个实际的可中断解析锁,下面先创建并运行存储过程PP:
scott@ORCL>create or replace procedure pp as begin null; end;
  2  /
过程已创建。

scott@ORCL>exec pp
PL/SQL 过程已成功完成。
过程PP现在会出现在DBA_DDL_LOCKS视图中。我们有这个过程的一个解析锁:
然后重新编译这个过程,并再次查询视图:
scott@ORCL>select session_id sid, owner, name, type,
  2  mode_held held, mode_requested request
  3  from dba_ddl_locks
  4  /


       SID OWNER			NAME				TYPE			HELD      REQUEST
---------- ------			--------------------		---------------------- -----	  -----
       131 SYS				DBMS_OUTPUT			Body      		Null      None
       190 SYS				ALERT_QUE			10         		Null      None
       131 SYS				DBMS_APPLICATION_INFO		Table/Procedure/Type    Null      None
       190 SYS				DBMS_HA_ALERTS_PRVT		Body              	Null      None
       131 SCOTT			PP				Table/Procedure/Type   	Null      None
......


已选择26行。
可以看到,现在这个视图中没有P了。我们的解析锁被中断了。
这个视图对开发人员很有用,发现测试或开发系统中某段代码无法编译时,将会挂起并最终超时。
这说明,有人正在使用这段代码(实际上在运行这段代码),你可以使用这个视图来查看这个人是谁。
对于GRANTS和对象的其他类型的DDL也是一样。
例如,无法对正在运行的过程授予EXECUTE权限。可以使用同样的方法来发现潜在的阻塞者和等待者。

3.闩

闩(latch)是轻量级的串行化设备,用于协调对共享数据结构、对象和文件的多用户访问。
闩就是一种锁,设计为只保持极短的一段时间(例如,修改一个内存中数据结构所需的时间)。
闩用于保护某些内存结构,如数据库块缓冲区缓存或共享池中的库缓存。
一般会在内部以一种“愿意等待”(willing to wait)模式请求闩。
这说明,如果闩不可用,请求会话会睡眠很短的一段时间,并在以后再次尝试这个操作。
还可以采用一种“立即”(immediate)模式请求其他闩,这与SELECT FOR UPDATE NOWAIT的 思想很相似,说明这个进程会做其他事情(如获取另一个与之相当的空闲闩),而不只是坐而等待这个闩直到它可用。
由于许多请求者可能会同时等待一个闩,你会 看到一些进程等待的时间比其他进程要长一些。
闩的分配相当随机。闩释放后,紧接着不论哪个会话请求闩都会得到它。等待闩的会话不会排 队,只是一大堆会话在不断地重试。


Oracle使用诸如“测试和设置”(test and set)以及“比较和交换”(compare and swap) 之类的原子指令来处理闩。
由于设置和释放闩的指令是原子性的,尽管可能有多个进程在同时请求它,但操作系统本身可以保证只有一个进程能测试和设置闩。
指令 仅仅是一个指令而已,它执行得可能非常快。闩只保持很短的时间,而且提供了一种清理机制,万一某个闩持有者在持有闩时异常地“死掉了”,就能执行清理。这 个清理过程由PMON执行。


队列锁(enqueue),也是一种更复杂的串行化设备,例如,在更新数据库表中的行时就会使用队列锁。
与闩的区别在于,队列锁允许请求者“排队”等待资源。
对于闩请求,请求者会话会立即得到通知是否得到了闩。而对于队列锁,请求者会话会阻塞,直至真正得到锁。

注意 使用SELECT FOR UPDATE NOWAIT或WAIT ,你还可以决定倘若会话被阻塞,则并不等待一个队列锁,但是如果确实阻塞并等待,就会在一个队列中等待。

因此,队列锁没有闩快,但是它确实能提供闩所没有的一些功能。可以在不同级别上得到队列锁,因此可以有多个共享锁以及有不同程度共享性的锁。

1. 闩“自旋”

闩是一种锁,锁是串行化设备,而串行化设备会妨碍可扩缩性。如果你的目标是构建一个能在Oracle环境中很好地扩缩的应用,就必须寻找合适的方法和解决方案,尽量减少所需执行的闩定的量。

有些活动尽管看上去很简单(如解析一条SQL语句),也会为共享池中的库缓存和相关结构得到并释放数百个或数千个闩。
如果我们有一个闩,可能会有另外某个人在等待这个闩。而当我们想要得到一个闩时,也许我们自己也必须等待(因为别人拥有着这个闩)。

等待闩可能是一个代价很高的操作。如果闩不是立即可用的,我们就得等待(大多数情况下都是如此),在一台多CPU机器上,我们的会话就会自旋(spin),也就是说,在循环中反复地尝试来得到闩。
出现自旋的原因是,上下文切换(context switching)的开销很大(上下文切换是指被“踢出”CPU,然后又必须调度回CPU)。
所以,如果进程不能立即得到闩,我们就会一直呆在CPU上,并立即再次尝试。
我们指望闩的持有者正在另一个CPU上忙于处理(由于闩设计为只保持很短的时间),而且会很快放弃闩。
如果出现自旋并不断地尝试想得到闩,但是之后还是得不到闩,此时我们的进程才会睡眠,或者让开CPU,而让其他工作进行。得到闩的伪代码如下所示:
Attempt to get Latch
If Latch gotten
Then
return SUCCESS
Else
Misses on that Latch = Misses+1;
Loop
Sleeps on Latch = Sleeps + 1
For I in 1 .. 2000
Loop
Attempt to get Latch
If Latch gotten
Then
Return SUCCESS
End if
End loop
Go to sleep for short period
End loop
End if
其逻辑是,尝试得到闩,如果失败,则递增未命中计数(miss count),这个统计结果可以在Statspack报告中看到,或者直接查询V$LATCH视图也可以看到。
一旦进程未命中,它就会循环一定的次数(有一个参数能控制这个次数,通常设置为2 000),反复地试图得到闩。
如果某次尝试成功,它就会返回,我们能继续处理。
如果所有尝试都失败了,这个进程就会将该闩的睡眠计数(sleep count)递增,然后睡眠很短的一段时间。醒来时,整个过程会再重复一遍。
这说明,得到一个闩的开销不只是“测试和设置”操作这么简单,我们尝试得到闩时,可能会耗费大量的CPU时间。系统看上去非常忙(因为消耗了很多CPU时间),但是并没有做多少实际的工作。


2. 测量闩定共享资源的开销

举个例子,我们来研究闩定共享池的开销。我们会把一个编写得很好的程序和一个编写得不太好的程序进行比较,前者使用了绑定变量,而在编写得不好的程序中,每条语句使用了SQL直接量或各不相同的SQL。
为此,我们使用了一个很小的Java程序,它只是登录Oracle,关掉自动提交,并通过一个循环执行25 000条不同的INSERT语句。我们会执行两组测试:在第一组测试中,程序不使用绑定变量;在第二组测试中,程序会使用绑定变量。
要评估这些程序以及它们在多用户环境中的行为,用Statspack来收集度量信息,如下:
(1) 执行一个Statspack快照来收集系统的当前状态。
(2) 运行程序的N个副本,每个程序向其自己的数据库表中插入(INSERT),以避免所有程序都试图向一个表中插入而产生的竞争。
(3) 在最后一个程序副本执行后,紧接着取另一个快照。
然后只需打印出Statspack报告,并查看完成N个程序副本需要多长时间,使用了多少CPU时间,主要的等待事件是什么,等等。


如果所要执行的处理不需要共享资源,这么说可能是正确的,但是我们的进程确实会使用一个共享资源,即共享池(Shared pool)。
我们需要闩定共享池来解析SQL语句,为什么要闩定共享池呢?因为这是一个共享数据结构,别人在读取这个共享资源时,我们不能对其进行修改,另外如果别人正在修改它,我们就不能读取。
1. 不使用绑定变量
在第一个实例中,我们的程序不使用绑定变量,而是使用串连接来插入数据:
import java.sql.*;
public class instest
{
	static public void main(String args[]) throws Exception{
		DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver());
		Connection conn = DriverManager.getConnection
											("jdbc:oracle:thin:@dellpe:1521:ora10gr1",
											"scott","tiger");
											conn.setAutoCommit( false );
		Statement stmt = conn.createStatement();
		for( int i = 0; i < 25000; i++ ){
			stmt.execute("insert into "+ args[0] + " (x) values(" + i + ")" );
		}
		conn.commit();
		conn.close();
	}
}

现在,如果要同时运行这样的两个程序,硬解析数比预计的稍微多了一点,但是CPU时间是原来的3倍而不是两倍!怎么会这样呢?答案就在于Oracle的闩定实现。
在这台多CPU机器上,无法立即得到一个闩时,我们就会“自旋”。自旋行为本身会消耗CPU时间。
进程1 多次尝试想要得到共享池的闩,但最终只是一再地发现进程2持有着这个闩,所以进程1必须自旋并等待(这会消耗CPU时间)。
反过来对进程2也是一样,通过多次尝试,它发现进程1正持有着所需资源的闩。
所以,很多处理时间都在等待某个资源可用。


为了完成两个单位的工作,这里需要使用3个单位的CPU时间。其原因就在于:我们需要一个共享资源(即共享池),这正是闩定的本质所在。
另外,由于存在这种自旋,通常不可能确定系统使用多少CPU时间,从这个两用户的测试所能知道的只是:我们使用了的CPU时间,而且力图得到共享池闩时未命中次数。
我们不知道每次得不到闩时要尝试多少次 ,所以没有具体的办法来度量有多少CPU时间花在自旋上,而有多少CPU时间用于处理。要得到这个信息,我们需要多个数据点。

使用了绑定变量
import java.sql.*;
public class instest
{
	static public void main(String args[]) throws Exception{
		DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver());
		Connection	conn = DriverManager.getConnection
											("jdbc:oracle:thin:@dellpe:1521:ora10gr1",
											"scott","tiger");
											conn.setAutoCommit( false );
		PreparedStatement pstmt = conn.prepareStatement("insert into "+ args[0] + " (x) values(?)" );
		for( int i = 0; i < 25000; i++ ){
			pstmt.setInt( 1, i );
			pstmt.executeUpdate();
		}
		conn.commit();
		conn.close();
	}
}
没有使用绑定变量时,我们的CPU时间中有5/6的时间都用于解析SQL。
这并非都是闩导致的,因为没有使用绑定变量时,解析和优化SQL也需要许多CPU时间。
解析SQL是CPU密集型操作(需要耗费大量CPU时间)。


查看Statspack报告中的闩部分时,发现,如果使用了绑定变量,则根本没有闩等待,对共享池和库缓存的竞争太少了,所以甚至没有相关的报告。

手动锁定和用户定义锁

更新一个表时,Oracle会为它加一个TM锁,以防止其他会话删除这个表(实际上,也会防止其他会话对这个表执行大多数DDL)。
在我们修改的各个块上会加上TX锁,这样就能告诉别人哪些数据是“我们的”。
数据库采用DDL锁来保护对象,这样当我们正在修改这些对象时,别人不会同时对它们进行修改。
数据库在内部使用了闩和锁(lock)来保护自己的结构。


介入这种锁定活动。有以下选择:
1. 通过一条SQL语句手动地锁定数据。
2. 通过DBMS_LOCK包创建我们自己的锁。


1. 手动锁定
SELECT...FOR UPDATE语句就是手动锁定数据的一种主要方法,来避免丢失更新问题(也就是一个会话可能覆盖另一个会话所做的修改)。
可以用这种方法来串行访问详细记录,从而执行业务规则。
还可以使用LOCK TABLE语句手动地锁定数据。这个语句实际上很少使用,因为锁的粒度太大。它只是锁定表,而不是对表中的行锁定。如果你开始修改行,它们也会被正常地“锁定”。
所以,这种方法不能节省资源(但在其他RDBMS中可以用这个方法节省资源)。
如果你在编写一个大批量的更新,它会影响给定表中的大多数行,而且你希望保证没有人能“阻塞”你,就可以使用LOCK TABLE IN EXCLUSIVE MODE语句。通过以这种方式锁定表,就能确保你的更新能够执行所有工作,而不会被其他事务所阻塞。


2. 创建你自己的锁
通过DBMS_LOCK包,Oracle实际上向开发人员公开了它在内部使用的队列锁(enqueue lock)机制。
例如,你可能要使用这个包对Oracle外部的一些资源进行串行访问。假设在使用UTL_FILE例程,它允许你写至服务器文件系统上的一个文件。你可能已经开发了一个通用的消息例程,每个应用都能调用这个例程来记录消息。
由于这个文件是外部的,Oracle不会对试图同时修改这个文件的多个用户进行协调。现在,由于有了DBMS_LOCK包,在你打开、写入和关闭文件之前,可以采用排他模式请求一个锁(以文件命名),一次只能有一个人向这个文件写消息。这样所有人都会排队。
通过利用DBMS_LOCK包,等你用完了锁之后能手动地释放这个锁,或者在你提交时自动放弃这个锁,甚至也可以在你登录期间一直保持这个锁。

猜你喜欢

转载自blog.csdn.net/a0001aa/article/details/80072078