一、 Oracle锁机制
1、什么是锁
锁是控制“共享资源”并发存取的一种机制。注意这里说“共享资源”而不仅指“数据行”,数据库的却在行一级对表的数据加锁,但是数据库也在其它地方对各种资源的并发存取使用锁。比如说,如果一个存储过程在执行过程中,它会被加上某种模式的锁只允许某些用户执行它而不允许其他用户修改它。锁在数据库中被用来实现允许对共享资源的并发存取,同时保证数据的完整性和一致性。
2、锁的类型
在数据库中有两种基本的锁类型:排它锁(Exclusive Locks,即X锁)和共享锁(Share Locks,即S锁)。当数据对象被加上排它锁时,其他的事务不能对它读取和修改(错误的,正确的应该是能读取,不能修改)。加了共享锁的数据对象可以被其他事务读取,但不能修改。数据库利用这两种基本的锁类型来对数据库的事务进行并发控制。
排他锁和共享锁的区别:对表加上排他锁后,其他事物不能再对该表加锁;对表加上共享锁后,其他事物可以再对该表加锁。共同享锁是允许多个用户在同一个表中放置多个共享锁的,而排他锁只允许一个用户在表中放置排他锁,所以说,如果我在一个表中放置了排他锁的话,别人就不能再在这个表中放置排他锁的。
oracle中加排他锁和共享锁的格式:
排他锁:lock table table_name in exclusive mode;
共享锁:lock table table_name in share mode;
根据保护的对象不同,Oracle数据库锁可以分为以下几大类:DML锁(data locks,数据锁),用于实现并发存取并保护数据的完整性;DDL锁(dictionary locks,字典锁),用于保护数据库对象的结构,如表、索引等的结构定义;内部锁和闩(internal locks and latches),保护数据库的内部结构,比如数据库解析了一条查询语句并生成了最优化的执行计划,它将把这个执行计划“latche”在library cache中然后供其它session使用。
DML锁的目的在于保证并发情况下的数据完整性,它也是我们最常见和常用的锁,本文我们主要讨论DML锁。在Oracle数据库中,DML锁主要包括TM锁和TX锁,其中TM锁称为表级锁(用来保证表的结构不被用户修改),TX锁称为事务锁或行级锁。当Oracle执行DML语句时,系统自动在所要操作的表上申请TM类型的锁。当TM锁获得后,系统再自动申请TX类型的锁,并将实际锁定的数据行的锁标志位进行置位。这样在事务加锁前检查TX锁相容性时就不用再逐行检查锁标志,而只需检查TM锁模式的相容性即可,大大提高了系统的效率。TM锁包括了SS、SX、S、X等多种模式,在数据库中用0-6来表示。不同的SQL操作产生不同类型的TM锁。如表1所示。
表1:Oracle的TM锁模式 | |||
锁模式 | 锁描述 | 解释 | SQL操作 |
0 | none | ||
1 | NULL | 空 | Select |
2 | SS(Row-S) | 行级共享锁,其他对象只能查询这些数据行 | Select for update、Lock for update、Lock row share |
3 | SX(Row-X) | 行级排它锁,在提交前不允许做DML操作 | Insert、Update、Delete and so on |
4 | S(Share) | 共享锁 | Create index、Lock share |
5 | SSX(S/Row-X) | 共享行级排它锁 | Lock share row exclusive |
6 | X(Exclusive) | 排它锁 | Alter table、Drop able、Drop index、Truncate table 、Lock exclusive |
在数据行上只有X锁(排他锁)。在 Oracle数据库中,当一个事务首次发起一个DML语句时就获得一个TX锁,该锁保持到事务被提交或回滚。当两个或多个会话在表的同一条记录上执行DML语句时,第一个会话在该条记录上加锁,其他的会话处于等待状态。当第一个会话提交后,TX锁被释放,其他会话才可以加锁。
当Oracle数据库发生TX锁等待时,如果不及时处理常常会引起Oracle数据库挂起,或导致死锁的发生,产生ORA-60的错误。这些现象都会对实际应用产生极大的危害,如长时间未响应,大量事务失败等。
3、监控锁的相关视图
表2:数据字典视图说明 | ||
视图名 | 描述 | 主要字段说明 |
v$session | 查询会话的信息和锁的信息。 | sid,serial#:表示会话信息。 program:表示会话的应用程序信息。 row_wait_obj#:表示等待的对象。 和dba_objects中的object_id相对应。 |
v$session_wait | 查询等待的会话信息。 | sid:表示持有锁的会话信息。 Seconds_in_wait:表示等待持续的时间信息。 Event:表示会话等待的事件。 |
v$lock | 列出系统中的所有的锁。 | Sid:表示持有锁的会话信息。 Type:表示锁的类型。值包括TM和TX等。 ID1:表示锁的对象标识。 lmode,request:表示会话等待的锁模式的信息。用数字0-6表示,和表1相对应。 |
dba_locks | 对v$lock的格式化视图。 | Session_id:和v$lock中的Sid对应。 Lock_type:和v$lock中的type对应。 Lock_ID1: 和v$lock中的ID1对应。 Mode_held,mode_requested:和v$lock中的lmode,request相对应。 |
v$locked_object | 只包含DML的锁信息,包括回滚段和会话信息。 | Xidusn,xidslot,xidsqn:表示回滚段信息。和v$transaction相关联。 Object_id:表示被锁对象标识。 Session_id:表示持有锁的会话信息。 Locked_mode:表示会话等待的锁模式的信息,和v$lock中的lmode一致。 |
二、 锁的探讨
在我们讨论之前先来看一个关于锁的问题,这些问题大多都是因为那些设计不好的应用程序错误的使用(或没有使用)数据库锁机制引起的。
1、更新丢失
“更新丢失”是一个典型的数据库问题,在所有的多用户环境都可能遇到。简单的描述下“更新丢失”的产生:
1)session1的一个事务查询一行数据展现给user1。
2)另一个session2的一个事务也查询同一行数据展现给user2。
3)然后user1通过应用程序更新并提交这行数据,他完成了整个事务。
4)User2也同样通过应用程序更新并提交这行数据,他也完成了整个事务。
上面的过程就会造成“更新丢失”,因为所有在第三步修改的数据全部都会丢失。一个典型的例子就是售票系统,比如一个用户(user1)在网上预定查询到1号位的票还没售出,同时另一用户(user2)在现场售票点查询也查到1号位票没售出。然后user1预订了这张票(即售票系统更新了数据库表中1号位的信息“已预订”),而这时user2又将这张票卖给了现场购票的人(即user2也成功更新1号位的信息“已售”,覆盖更新了user1的更新),等到user1去拿票的时候他预定的票却已经被卖出去了,这就是应用系统出现的一个严重的问题。
2、悲观锁
“悲观锁”实际上是一种使用锁的方式,即user1主观的认为会发生“更新丢失”,所以在他查询的时候就对查询结果的数据“立刻”加锁来防止发生“更新丢失”。这是一种“悲观”的想法,所以叫做“悲观锁”。
“悲观锁”一般用于独占连接的数据库环境,至少是一个用户在一个事务的生存周期中独占这个连接,比如C/S这种结构的系统中。下面模拟下应用中如何使用“悲观锁”:
Session1:
//session1应用程序先查询信息(不加锁)
SQL> select * from test1;
ID NAME SEX
---------- ------------------------------ --------------------
100 iceberg3521 male
101 singlelove male
102 myself male
103 fengzhu male
104 test female
//session1的用户想修改id=102的这条记录,取出这条记录的值绑定到变量
SQL> variable id number
SQL> variable name varchar2(30)
SQL> variable sex varchar2(20)
SQL> exec :id :=102; :name :='myself'; :sex :='male';
PL/SQL 过程已成功完成。
//再简单查询看要修改的行是否已被其它session修改,并对要修改的行加锁,这里使用select for update nowait来对需要修改的行进行加锁。
SQL> select * from test1 where id=:id
2 and name=:name
3 and sex=:sex
4 for update nowait;
ID NAME SEX
---------- ------------------------------ --------------------
102 myself male
这里session1重复查询并对准备修改的行加锁来防止其它session来修改这行,这种方法就叫做“悲观锁”,因为我们悲观的人为从我们查询到修改这段时间会有其他人来修改我们打算修改的记录。
这一步实际会有三种结果:
1)102这条记录没有被其他人修改,我们就重新查询出来这条记录并成功对其加锁。
2)102这条记录正在被人修改,我们加得到下面这个结果:
SQL> select * from test1 where id=:id
2 and name=:name
3 and sex=:sex
4 for update nowait;
select * from test1 where id=:id
*
第 1 行出现错误:
ORA-00054: 资源正忙, 但指定以 NOWAIT 方式获取资源
3)如果102这条记录已经被人修改,我们的查询将返回0;这样我们的应用程序就需要重新查询来确定需要修改的记录,这样我们也不会重新修改别人修改的记录。
//一但我们成功加锁,我们就可以放心的修改这条记录了
SQL> update test1 set name='fz' where id=102;
已更新 1 行。
SQL> commit;
提交完成。
3、乐观锁
第二中方式就是“乐观锁”,这种方式在修改前才对要修改的数据加锁。即是说我们乐观的人为从我们查询到修改数据这段时间不会有其他人修改这条数据,我们直到修改前最后一刻才判断数据是否已被其他人修改过。
“乐观锁”适用于任何系统环境,但是这种方式出现“更新丢失”的几率比“悲观锁”大。
一种常用的实现“乐观锁”的方式就是应用程序保留查询出的旧值一直到更新的时候,然后象这样做:
Update table
Set column1 = :new_column1, column2 = :new_column2, ....
Where primary_key = :primary_key
And column1 = :old_column1
And column2 = :old_column2
...
上面这样做如果更新返回的结果行数为0则说明其他人已经修改了这行记录,然后我们需要告诉应用程序下一步该如何做(是重新查询重复要做的事务还是执行其它)。还有一种情况是如果其他人正在修改同样的记录,我们的update将被hang住直到别人提交或是回滚。
还有很多实现“乐观锁”的方法,这里我们介绍使用一种由触发器或者应用程序管理的特殊字段来帮我们判断要修改记录的“版本”的方法。
这是一种简单的实现方式,通过在表中增加一个number类型的字段或者date和timestamp类型的字段来防止“更新丢失”的发生。这些字段通常由触发器来维护负责增加number字段的值或者更新date/timestamp字段的日期时间。
只要应用程序保存这个特殊字段的值,然后在更新前一刻比较表中这行数据的这个个字段的值是否与前面读取保存的这个值相等来判断记录是否被更改过。下面简单模拟下实现过程:
先给test1增加时间类型字段:
SQL> alter table test1 add last_mod timestamp
2 with time zone default systimestamp
3 not null;
表已更改。
注:timestamp with time zone这个数据类型只有9i以上才支持
//保存查询值到变量
SQL> variable id number
SQL> variable name varchar2(30)
SQL> variable sex varchar2(20)
SQL> variable last_mod varchar2(50)
SQL> begin
2 select name,sex,last_mod into :name,:sex,:last_mod
3 from test1 where id=102;
4 end;
5 /
PL/SQL 过程已成功完成。
//查询变量是否赋值
SQL> select :name,:sex,:last_mod from dual;
:NAME :SEX
-------------------------------- --------------------------------
:LAST_MOD
-----------------------------------------------------------------------
fz male
23-3月 -08 02.25.11.687000 下午 +08:00
//开始更新记录,使用last_mod来判断要更新的记录是否改变(这里要用到一个oracle内置函数TO_TIMESTAMP_TZ来转换:last_mod这个变量的值)。
SQL> update test1
2 set name='myself',last_mod=systimestamp
3 where id=102 and last_mod=to_timestamp_tz(:last_mod);
已更新 1 行。
//我们模拟在上面执行update之前如果这条记录已经被更改过,我们看到下面的结果(重复执行上面的update语句):
SQL> update test1
2 set name='myself',last_mod=systimestamp
3 where id=102 and last_mod=to_timestamp_tz(:last_mod);
已更新0行。
这里我们可以看到更新的记录行数为0,即我们没有更新到想要更新的这条记录,这样应用程序就可以判断出这条记录已经被别人更新过并知道接下来该如何做。
注:一般建议以上整个更新过程放在一个存储过程中来实现,不要在应用程序中直接实现此类逻辑,因为这样做将增加程序的代码量以及不便于后期维护。
4、用悲观锁还是乐观锁?
根据经验来看在oracle中使用“悲观锁”比使用“乐观锁”要好,但是“悲观锁”需要应用程序和oracle数据库是完整的独连的(因为不能跨连接加锁),通常用于C/S结构的系统。对现在很多B/S系统一般都使用“乐观锁”,但是完成这个事务的需要更大的开销。
5、阻塞
一个session1在一个资源上加了锁,而另一个session2同也需要占用该资源,这种情况下就会发生阻塞。Session2将会被hang住直到session1解锁释放资源。通常阻塞是可以避免的,如果你发现在你的交互式系统中发生了阻塞,那么很有可能你的系统也会发生“更新丢失”这种情况。这说明你的应用系统逻辑上有缺陷才导致阻塞的发生。
五种常用的DML语句都会发生阻塞(insert、update、delete、merge、select for update)。对于select for update来说只要加上nowait就可以避免阻塞,这点前面我们实验过,下面我们看看其它DML语句发生阻塞的情况:
1)阻塞insert
通常很少会在insert的时候发生阻塞,一种情况会发生在两个session同时对有主键或唯一索引的表插入相同记录时,这时只有当第一个session提交或者回滚后第二个session才解除阻塞;另一种情况会发生在有参照完整性约束的表中,当对子表insert的时候,如果与之想关联的父表的记录正在创建或删除,那么此时对子表insert的session将被阻塞。
对于第一种发生insert阻塞的情况,可以使用序列来生成主键或有唯一索引列的值,从而避免阻塞(序列就是在多用户和高并发操作系统环境中用来生成唯一值的)。
2)阻塞merge、update、delete
当两个session同时更新同一个表的同一条(或一组)记录会就会发生阻塞,使用我们前面讨论的“悲观锁”和“乐观锁”就可以避免update的阻塞。Merge实际上就是insert或update所以情况和处理方式跟insert和update一样。当一个session正update的时候另一个session delete同一条记录也会发生阻塞,效果同两个update的情况一样。
6、死锁
1)死锁发生在两个session同时锁住了对方正请求的资源的情况下,演示情况如下:
//2个表test2和test3,session1更新test2
SQL> update test2 set name='test2' where id=7;
已更新 1 行。
//session2更新test3
SQL> update test3 set name='test3' where id=100;
已更新 1 行。
//session2又更新test2中session1正在更新的行
SQL> update test2 set name='test3' where id=7;
此时session2阻塞
//session1又更新test3中session2正在更新的行
SQL> update test3 set name='test2' where id=100;
此时session1也被阻塞且session2报错:
SQL> update test2 set name='test3' where id=7;
update test2 set name='test3' where id=7
*
第 1 行出现错误:
ORA-00060: 等待资源时检测到死锁
如果此时session1不commit或rollback,session2将一直被阻塞。当session1提交或回滚后,session2将更新成功:
SQL> update test3 set name='test2' where id=100;
已更新 1 行。
Oracle人为死锁的出现是非常罕见的,所以在系统出现死锁后oracle会自动创建一个trace文件记录死锁信息,部分内容如下:
*** 2008-03-24 19:59:09.265
*** ACTION NAME:() 2008-03-24 19:59:09.218
*** MODULE NAME:(SQL*Plus) 2008-03-24 19:59:09.218
*** SERVICE NAME:(SYS$USERS) 2008-03-24 19:59:09.218
*** SESSION ID:(207.11) 2008-03-24 19:59:09.218
DEADLOCK DETECTED
[Transaction Deadlock]
Current SQL statement for this session:
update test2 set name='test3' where id=7
The following deadlock is not an ORACLE error. It is a
deadlock due to user error in the design of an application
or from issuing incorrect ad-hoc SQL. The following
information may aid in determining the deadlock:
Deadlock graph:
---------Blocker(s)-------- ---------Waiter(s)---------
Resource Name process session holds waits process session holds waits
TX-0006001d-00000372 30 207 X 22 190 X
TX-00040000-0000036c 22 190 X 30 207 X
session 207: DID 0001-001E-00000006 session 190: DID 0001-0016-0000000C
session 190: DID 0001-0016-0000000C session 207: DID 0001-001E-00000006
Rows waited on:
Session 190: obj - rowid = 0000CC86 - AAAMyGAAFAAAAEcAAA
(dictionary objn - 52358, file - 5, block - 284, slot - 0)
Session 207: obj - rowid = 0000C912 - AAAMkSAAFAAAABUAAA
(dictionary objn - 51474, file - 5, block - 84, slot - 0)
Information on the OTHER waiting sessions:
Session 190:
pid=22 serial=12 audsid=2465 user: 54/ICEBERG
O/S info: user: IceBerg, term: FENGZHU, ospid: 2300:2436, machine: FDJXINXI\FENGZHU
program: sqlplus.exe
application name: SQL*Plus, hash value=3669949024
Current SQL Statement:
update test3 set name='test2' where id=100
End of information on OTHER waiting sessions.
2)还有一种情况发生死锁是由没有索引的外键引起的,oracle会在下面两种情况给整个子表加锁:
l 当更新父表主键的时候,如果子表的外键没有索引则会给整个子表加锁。
l 还有就是删除父表记录的时候,同样也会给子表加锁。
比如这样做:
//session1
SQL> create table c (x references p);
表已创建。
SQL> insert into p values(1);
已创建 1 行。
SQL> insert into p values(2);
已创建 1 行。
SQL> commit;
提交完成。
SQL> insert into c values(2);
已创建 1 行。
//session2
SQL> delete from p where x=1;
此时session2立刻被阻塞,此时其它session(如session3)都不能够再对c表做insert、delete及update操作,如果执行这样的操作则会立刻被阻塞并当提交或回滚session1时session2就会提示发现死锁:
//session3
SQL> insert into c values(1);
被阻塞
//session1
SQL> rollback;
回退已完成。
//session2
SQL> delete from p where x=1;
delete from p where x=1
*
第 1 行出现错误:
ORA-00060: 等待资源时检测到死锁
#实际上此时并没有死锁,但是当其它sesion(如session3)在更新c表之前占有了其它资源,而此时session1又去请求这个资源时就会造成死循环从而导致死锁。
如果给c表的外键加了索引则可以避免死锁:
//session1
SQL> create index idx_c on c (x);
索引已创建。
SQL> insert into c values(1);
已创建 1 行。
//session2
SQL> delete from p where x=1;
此时还是会被阻塞
//session3
SQL> insert into c values(1);
同样被阻塞
//session1
SQL> rollback;
回退已完成。
//session2提示违反完整性约束而不会产生死锁
SQL> delete from p where x=1;
delete from p where x=1
*
第 1 行出现错误:
ORA-02292: 违反完整约束条件 (ICEBERG.SYS_C005335) - 已找到子记录
建议:能够在程序里实现数据完整性约束就尽量不要使用主键和外键关联。