鎖機制的分類
今天我們來了解Oracle中一項重要的機制,鎖機制,它在允許最大並發性能的前提下保證數據的一致與完整。很多文章在說到鎖機制時,往往寫得特別復雜,在各種鎖之外,又引入了所謂的”意向鎖”等等,同時在該詳細的地方,比如鎖的兼容性方面,缺乏進一步的解釋。所以我傾向”簡單粗暴”風格,盡量把內容往簡單的寫。我們先來看看Oracle鎖機制的基本分類。
1)DML locks
2)DDL locks
3)Internal locks and latches,內部鎖及閂,保護內部數據庫結構
4)Distributed locks,分布式鎖
5)PCM locks,並行高速緩存管理鎖
看起來很復雜的樣子,不過我們今天的主要內容是DML locks,其他的暫時略過不表,DML locks又可以分成:
1)TX鎖,即事務鎖(行級鎖)
2)TM鎖,即表及鎖
對於TX鎖,其實只有一個模式,即排他模式(exclusive,下稱X鎖),並不特別復雜,TM鎖相對復雜一點。
鎖機制的基本示例
SESS#132 SQL> insert into t_lock_1 values(1, 'a'); 1 row inserted SESS#132 SQL> select sid,type,id1,lmode, request from v$lock l where l.SID in (132, 202) and l.TYPE in ('TM','TX'); SID TYPE ID1 LMODE REQUEST ---------- ---- ---------- ---------- ---------- 132 TM 109413 3 0 132 TX 131075 6 0
v$lock的lmode相應數值的含義是:
0—none,
1—null,
2—row share (RS), 或sub share(SS),下稱SS
3—row exclusive (RX), 或sub exclusive(SX),下稱SX
4—share (S),
5—share row exclusive (SRX), 或share sub exclusive (SSX),下稱SSX
6—exclusive (X).
Oracle在DML時自動獲得的表級鎖只有SX一種模式,其它的模式需要通過手工的Lock table才能獲得;自動獲得的事務鎖也只有一種模式:X。
另外可以看到,當我們插入一條數據(未提交)時,獲得了兩個鎖,一個表級的SX,表明此表下的某些行正在被更改;另一個事務級的X,表明此數據行正在被我獨占。
SESS#132 SQL> insert into t_lock_1 values(2, 'b'); 1 row inserted SESS#132 SQL> select sid,type,id1,lmode,request from v$lock l where l.SID in (132, 202) and l.TYPE in ('TM','TX'); SID TYPE ID1 LMODE REQUEST ---------- ---- ---------- ---------- ---------- 132 TM 109413 3 0 132 TX 196633 6 0
我們插入了第二行記錄,表級鎖數量未增加,因為是操作的是同一表; 事務鎖數量也未增加,因為它們是在同一事務中。
SESS#202 SQL> insert into t_lock_1 values(3, 'c'); 1 row inserted SESS#202 SQL> select sid,type,id1,lmode, request from v$lock l where l.SID in (132, 202) and l.TYPE in ('TM','TX'); SID TYPE ID1 LMODE REQUEST ---------- ---- ---------- ---------- ---------- 202 TM 109413 3 0 132 TM 109413 3 0 202 TX 655370 6 0 132 TX 196633 6 0
在SESS#202中插入了第三條記錄,這次我們看到了四條鎖記錄。同時,我們也看到了,表級的SX鎖是相容的,這個下文會有解釋。
SESS#132 3:12:18 PM SQL> commit; Commit complete SESS#132 3:12:22 PM SQL> update t_lock_1 set val='aa' where id=1; 1 row updated SESS#202 2:45:08 PM SQL> commit; Commit complete SESS#202 3:13:25 PM SQL> update t_lock_1 set val='aa' where id=1;
更新同一條記錄,此時SESS#202被阻塞
SESS#132 3:15:05 PM SQL> select sid, type, id1,lmode, request from v$lock l where l.SID in (132, 202) and l.TYPE in ('TM','TX'); SID TYPE ID1 LMODE REQUEST ---------- ---- ---------- ---------- ---------- 202 TX 524318 0 6 132 TM 109413 3 0 202 TM 109413 3 0 132 TX 524318 6 0
可以看見SESS#202成功獲得了表級的SX,但在獲得事務級鎖時出現問題。這里我們看request列,request列即表示session希望獲得的鎖的類型,第一行中的6表示SESS#202希望獲得事務級的X鎖,而lmode字段的0則表示實際上未獲得。
SESS#132 3:18:59 PM SQL> select event, seconds_in_wait, sid from v$session_wait where sid in (132,202); EVENT SECONDS_IN_WAIT SID -------------------------------- --------------- ---------- SQL*Net message from client 0 132 enq: TX - row lock contention 325 202
可以看到SESS#202的等待事件,enq即enquence,表示排隊等待,TX – row lock contention表明在事務鎖上存在爭用。
SESS#132 3:24:12 PM SQL> select s1.username || '@' || s1.machine || ' ( SID=' || s1.sid || ' ) is blocking ' || s2.username || '@' || s2.machine || ' ( SID=' || s2.sid || ' ) ' AS blocking_status from v$lock l1, v$session s1, v$lock l2, v$session s2 where s1.sid = l1.sid and s2.sid = l2.sid and l1.BLOCK = 1 and l2.request > 0 and l1.id1 = l2.id1 and l2.id2 = l2.id2; BLOCKING_STATUS -------------------------------------------------------------------------------- SYSTEM@APAC\L00056378 ( SID=132 ) is blocking SYSTEM@APAC\L00056378 ( SID=202 )
用上面的查詢可以清楚地看到誰在阻塞誰
SESS#132 3:25:35 PM SQL> commit; Commit complete SESS#202 3:25:40 PM SQL> commit; Commit complete
上面列舉的修改同一行數據造成的阻塞是我們最常見到的,還有一些情況也會導致阻塞,比如:SESS#1向表的主鍵列插入一值但未提交,SESS#2向此主鍵列插入相同值,則SESS#2被阻塞。有外鍵約束的表也存在類似的情況,即SESS#1向父表插入一值但未提交,SESS#2嘗試引用此值,則SESS#2被阻塞。
Select for update語句與鎖
SQL> select * from t_lock_1 for update; SQL> select sid,type,id1,lmode,request from v$lock l where l.SID =74 and l.TYPE in ('TM','TX'); SID TYPE ID1 LMODE REQUEST ---------- ---- ---------- ---------- ---------- 74 TM 109413 3 0 74 TX 458763 6 0
很多文章中涉及select for update都表示這個語句會獲得事務級的X鎖,及表級的SS鎖,后者其實是不正確的,從9.2.0.5以后,其在表級別上獲得的就是SX鎖,參見上面的查詢。也就是說,它跟DML語句獲得的鎖是一樣的。
Lock table命令與鎖
到目前為止,我還很少在實際的應用開發中使用過Lock table,事實上,Oracle也不推薦對表的手工Lock,不過為了文章內容的完整,這里也簡單做一些介紹。其中lock mode主要包括lmode中列出的幾種。
前面我們說到Oracle自動獲取的表級鎖的只有SX一種,但是使用Lock table語句則可以將表置於其它任意一種鎖模式中。
關於Lock table語句中的[nowait | wait n]選項,可以參見下面的示例,我們在SESS#1中將表lock,SESS#2嘗試鎖定同一表,在nowait的情況下,lock table語句立即出錯,而在wait 10的情況下,語句等待了10秒中,之后才報錯。
SESS#1 4:55:54 PM SQL> lock table t_lock_1 in row exclusive mode; Table(s) locked SESS#2 4:56:04 PM SQL> lock table t_lock_1 in exclusive mode nowait; lock table t_lock_1 in exclusive mode nowait ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired SESS#2 4:56:05 PM SQL> SESS#2 4:58:10 PM SQL> lock table t_lock_1 in exclusive mode wait 10; lock table t_lock_1 in exclusive mode wait 10 ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired SESS#2 4:58:21 PM SQL>
誰占用了鎖?
通常,如果在發生鎖的時候能得知是哪個用戶造成的,無疑更有利於問題的解決。這時,需要借助v$session視圖。
SQL> select vl.SID,vl.ID1,vl.LMODE,vs.USERNAME, vs.OSUSER,vs.MACHINE,vs.PROGRAM,vs.PROCESS from v$lock vl join v$session vs on vl.SID=vs.SID and vl.type='TM'; SID ID1 LMODE USERNAME OSUSER MACHINE PROGRAM PROCESS ---------- ---------- ---------- ---------- -------------------- -------------------- -------------------- ------------------------ 15 109425 3 SYSTEM APAC\Morven.Huang APAC\L00056378 plsqldev.exe 7824:7828
有了v$session視圖中數據的幫助,我們就能確定具體是誰阻塞了大家。甚至於,借助v$session視圖中的process字段,我們還能更詳細地知道是哪個進/線程,以上述查詢結果為例,如果你打開了多個plsqldev,可以借助process字段確定引發阻塞的語句是在哪個plsqldev進/線程中執行的。
當然,我們也可以關聯v$process視圖(v$session.paddr = v$process.addr)查看更加詳細的進程信息(雖然它們並沒有太大的用處)。
另外,我們也可以具體查看是哪些對象被鎖了,對於表級鎖,v$lock中的字段ID1即object_id,我們可以關聯系統字典dba_objects來得到對象信息。
SQL> select vl.sid, vl.type, vl.id1,vl.lmode, do.object_name,do.object_type from v$lock vl join dba_objects do on vl.ID1=do.object_id and type='TM'; SID TYPE ID1 LMODE OBJECT_NAME OBJECT_TYPE ---------- ---- ---------- ---------- --------------- ------------------- 15 TM 109425 3 T_LOCK_3 TABLE
表級鎖兼容性的解釋
解釋一下,首先,我們說,就嚴格程度而言,X鎖高於S鎖,而對象鎖又高於子對象鎖,這兩點應該沒什么異議。
前面說過,SS與SX中第一個字母是指的Sub,即子對象(我們可以把行理解成表的子對象),因此SS與SX級別最低,S次之,而SSX可以理解成S+SX,比S鎖高,X則毫無疑問是最高的。這就是表格第一列從上到下的順序了。
再來看兼容性問題,SS,SX互相兼容,因為,無論SS,SX,實際上表示的是表中的一部分數據行被鎖,如果其他用戶請求表中另外的數據行,Oracle沒有理由拒絕,從保證並發性能來講,它們也必須兼容。有人要問:如果兩個人申請鎖定的是表相同的數據行怎么辦?沒關系,這里我們討論的是表級鎖,我們還有事務鎖,由它來控制。
SX與S不相容,S即Share,只有存在多人,才有所謂的共享,那么,多人同時查看一張表,如果其中一個人修數據或者表結構,他一定會被其他人鄙視的,所以這種情況下,SX與S不能兼容。那么,如果只有一個SESSION持有S鎖,這個SESSION可以修改數據嗎?答案是可以,因為只有一個SESSION持有鎖的情況下,實際上也就無所謂”共享”了。
SESS#136 12:55:05 PM SQL> select sid from v$session where audsid=userenv('SESSIONID'); SID ---------- 136 SESS#75 12:55:11 PM SQL> select sid from v$session where audsid=userenv('SESSIONID'); SID ---------- 75 SESS#136 12:55:46 PM SQL> lock table t_lock_4 in share mode; Table(s) locked SESS#75 12:56:13 PM SQL> update t_lock_4 set val='aa' where id=1;
SESS#75中的update語句被阻塞。Ctrl+C中止SESS#75中的DML。
SESS#136 12:57:56 PM SQL> select * from v$lock where sid=136 and type in ('TM','TX'); ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ---------- ---- ---------- ---------- ---------- ---------- ---------- ---------- 0F346634 0F346664 136 TM 109427 0 4 0 143 0 2A49D1E8 2A49D228 136 TX 262149 12143 6 0 143 0 SESS#136 12:58:11 PM SQL> update t_lock_4 set val='aa' where id=1; 1 row updated 12:59:28 PM SQL> select * from v$lock where sid=136 and type in ('TM','TX'); ADDR KADDR SID TYPE ID1 ID2 LMODE REQUEST CTIME BLOCK -------- -------- ---------- ---- ---------- ---------- ---------- ---------- ---------- ---------- 0F346634 0F346664 136 TM 109427 0 5 0 2 0 2A49D1E8 2A49D228 136 TX 262149 12143 6 0 223 0
此時,表級鎖已經升級成5,即SSX
DBMS_LOCK的使用
DBMS_LOCK是Oracle提供給用戶用於自定義鎖的一個包,下面做一個示例,利用鎖機機制模擬讓兩條語句同時執行。當然,DBMS_LOCK的本職用途是在開發時控制多個進線程間的同步,這跟C#中的鎖機制是一樣的。
SESS#1
declare v_lockhandle varchar2(200); v_result number; begin dbms_lock.allocate_unique('control_lock', v_lockhandle); v_result := dbms_lock.request(v_lockhandle, dbms_lock.x_mode); end;
SESS#2
declare v_result number; v_lockhandle varchar2(200); begin dbms_lock.allocate_unique('control_lock', v_lockhandle); v_result := dbms_lock.request(v_lockhandle, dbms_lock.ss_mode); insert into t_lock_5 values (2, systimestamp); commit; end;
SESS#3
declare v_result number; v_lockhandle varchar2(200); begin dbms_lock.allocate_unique('control_lock', v_lockhandle); v_result := dbms_lock.request(v_lockhandle, dbms_lock.ss_mode); insert into t_lock_5 values (3, systimestamp); commit; end;
此時SESS#2, SESS#3都被阻塞。
SESS#1 (釋放鎖)
declare v_lockhandle varchar2(200); v_result number; begin dbms_lock.allocate_unique('control_lock', v_lockhandle); v_result := dbms_lock.release(v_lockhandle); end;
查看表t_lock_5中的結果
SQL> select * from t_lock_5; SID TS ---------- ------------------------------------------------- 2 20-SEP-12 04.16.59.133000 PM 3 20-SEP-12 04.16.59.133000 PM
示例很簡單,DBMS_LOCK提供了申請鎖(request)與釋放鎖(release)的方法,另外, allocate_unique提供輔助功能,用戶可以用它將鎖的名稱(上面的“control_lock”)轉換成lock handle,以便申請或釋放的時候使用。另外,DBMS_LOCK包中還有一個sleep過程,與C#中Thread.sleep類似,也是將進線程置於休眠狀態,可以通過參數指定具體的休眠時間(單位為秒)。