一、 Oracle鎖機制
1、什麼是鎖
鎖是控制“共享資源”併發存取的一種機制。注意這裏說“共享資源”而不僅指“數據行”,數據庫的卻在行一級對錶的數據加鎖,但是數據庫也在其它地方對各種資源的併發存取使用鎖。比如說,如果一個存儲過程在執行過程中,它會被加上某種模式的鎖只允許某些用戶執行它而不允許其他用戶修改它。鎖在數據庫中被用來實現允許對共享資源的併發存取,同時保證數據的完整性和一致性。
2、鎖的類型
在數據庫中有兩種基本的鎖類型:排它鎖(Exclusive Locks,即X鎖)和共享鎖(Share Locks,即S鎖)。當數據對象被加上排它鎖時,其他的事務不能對它讀取和修改。加了共享鎖的數據對象可以被其他事務讀取,但不能修改。數據庫利用這兩種基本的鎖類型來對數據庫的事務進行併發控制。
根據保護的對象不同,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) - 已找到子記錄
建議:能夠在程序裏實現數據完整性約束就儘量不要使用主鍵和外鍵關聯。