標籤:
原文地址:http://www.uml.org.cn/sjjm/201205302.asp
00 – 基本概念
當並發事務同時訪問一個資源的時候,有可能導致資料不一致。因此需要一種致機制來將訪問順序化。
鎖就是其中的一種機制。我們用商場的試衣間來做一個比喻。試衣間供許多消費者使用。因此可能有多個消費者同時要試衣服。為了避免衝突,試衣間的門上裝了鎖。試衣服的人在裡邊鎖住,其他人就不能從外邊開啟了。只有裡邊的人開門出來,外邊的人才能進去。
- 鎖的基本類型
資料庫上的操作可以歸納為兩中,讀和寫。多個事務同時讀一個對象的時候,是不會有衝突的。
同時讀和寫或者同時寫才會產生衝突。因此為了提高並發性,通常定義兩種鎖:
A. 共用鎖定(Shared Lock) 也叫讀鎖.
共用鎖定表示對資料進行讀操作。因此多個事務可以同時為一個對象加共用鎖定。
B. 獨佔鎖定(Exclusive Lock) 也叫寫鎖.
獨佔鎖定表示對資料進行寫操作。如果一個事務對對象加了獨佔鎖定,其他事務就不能再給它加任何鎖了。
- S、X鎖的相容性矩陣
對於鎖,通常會用一個矩陣來描述他們之間的衝突關係。
S X
S + –
X - -
+ 代表相容, -代表不相容
- 鎖的粒度
A. 表鎖(Table Lock)
對整個表加鎖,影響標準的所有記錄。通常用在DDL語句中,如DELETE TABLE,ALTER TABLE等。
B. 行鎖(Row Lock)
對一行記錄加鎖,隻影響一條記錄。通常用在DML語句中,如INSERT, UPDATE, DELETE等。
很明顯,表鎖影響整個表的資料,因此並發性不如行鎖好。
- 意圖鎖定(Intention Lock)
因為表鎖覆蓋了行鎖的資料,所以表鎖和行鎖也會產生衝突。如:
A. trx1 BEGI
B. trx1 給 T1 加X鎖,修改表結構。
C. trx2 BEGIN
D. trx2 給 T1 的一行記錄加S或X鎖(事務被阻塞,等待加鎖成功)。
trx1要操作整個表,鎖住了整個表。那麼trx2就不能再對T1的單條記錄加X或S鎖,去讀取或修這條記錄。
為了方便檢測表級鎖和行級鎖之間的衝突,就引入了意圖鎖定。
A. 意圖鎖定分為意向讀鎖(IS)和意向寫鎖(IX)。
B. 意圖鎖定是表級鎖,但是卻表示事務正在讀或寫某一行記錄,而不是整個表。
所以意圖鎖定之間不會產生衝突,真正的衝突在加行鎖時檢查。
C. 在給一行記錄加鎖前,首先要給該表加意圖鎖定。也就是要同時加表意圖鎖定和行鎖。
採用了意圖鎖定後,上面的例子就變成了:
A. trx1 BEGIN
B. trx1 給 T1 加X鎖,修改表結構。
C. trx2 BEGIN
D. trx2 給 T1 加IX鎖(事務被阻塞,等待加鎖成功)
E. trx2 給 T1 的一行記錄加S或X鎖.
- 表鎖的相容性矩陣
IS IX S X
IS + + + –
IX + + - -
S + - + -
X - - - -
+ 代表相容, -代表不相容
A. 意圖鎖定之間不會衝突, 因為意圖鎖定僅僅代表要對某行記錄進行操作。在加行鎖時,會判斷是否衝突。
01 – 行鎖
直觀的理解,行鎖就是要鎖住一行記錄,阻止其他事務操作該行記錄。這裡有一個隱含的邏輯:
A. 插入操作永遠不會被阻止,因為插入操作不會操作一條存在的記錄(這裡不考慮Insert duplicate的處理)。這個邏輯是對的嗎? 這和使用者的使用方式相關,有些情況下是使用者能接受的,有些情況下是使用者不能接受的。
- 幻讀(Phantom Read)
如果不阻止INSERT操作,就會產生幻讀.MySQL手冊中有幻讀的介紹.
A. MVCC 可以避免幻讀.但是MVCC只對SELECT語句有效,對於SELECT … [LOCK IN SHARE MODE | FOR UPDATE], UPDATE, DELETE語句無效。
B. 為了能夠通過鎖避免幻讀,採用了next-key的機制。next-key通過鎖住2個記錄之間的間隙,來阻止INSERT操作。
- 行鎖的模式
行鎖S、X鎖上做了一些精確的細分,在代碼中稱作Precise Mode。這些精確的模式,使的鎖的粒度更細小。可以減少衝突。
A. 間隙鎖(Gap Lock),只鎖間隙。
B. 記錄鎖(Record Lock) 只鎖記錄。
C. Next-Key Lock(代碼中稱為Ordinary Lock),同時鎖住記錄和間隙.
D. 插入意圖鎖(Insert Intention Lock),插入時使用的鎖。在代碼中,插入意圖鎖,
實際上是GAP鎖上加了一個LOCK_INSERT_INTENTION的標記.
MySQL手冊對這些模式有詳細的介紹.
- 行鎖模式的相容性矩陣
G I R N (已經存在的鎖,包括等待的鎖)
G + + + +
I - + + -
R + + - -
N + + - -
+ 代表相容, -代表不相容. I代表插入意圖鎖,
G代表Gap鎖,I代表插入意圖鎖,R代表記錄鎖,N代表Next-Key鎖.
S鎖和S鎖是完全相容的,因此在判別相容性時不需要對比精確模式。
精確模式的檢測,用在S、X和X、X之間。
這個矩陣是從lock0lock.c:lock_rec_has_to_wait()的代碼推出來的。從這個矩陣可以看到幾個特點:
A. INSERT操作之間不會有衝突。
B. GAP,Next-Key會阻止Insert。
C. GAP和Record,Next-Key不會衝突
D. Record和Record、Next-Key之間相互衝突。
E. 已有的Insert鎖不阻止任何準備加的鎖。
同時也有幾個疑問:
A. 為什麼插入意圖鎖不阻止間隙鎖?在特定的情況下會導致INSERT操作被無限期延遲。
B. 如果不阻止任何鎖,這個鎖還有必要存在嗎?
- 目前看到的作用是,通過加鎖的方式來喚醒等待線程。
- 但這並不意味著,被喚醒後可以直接做插入操作了。需要再次判斷是否有鎖衝突。
C. GAP+LOCK_INSERT_INTENTION標記的方式,能否直接變成INSERT_INTENTION鎖?
目前還在看。
- B+Tree 行鎖
InnoDB的行鎖並不是簡單的資料行鎖的概念。而是指每個B+Tree上的行鎖,也可以理解為每個Index上的行鎖。因此操作一行記錄時,有可能會加多個行鎖在不同的B+Tree上。如:
CREATE TABLE t1(c1 INT KEY, c2 int, c3 int, INDEX(c2));
INSERT INTO t1 VALUES(1, 1, 1), (3, 3, 3)
UPDATE t1 c3 = 10 WHERE c2 <= 2
UPDATE語句會同時在Secondary Index和Clustered Index上加鎖。
- 行鎖模式的使用
行鎖的這些模式都在什麼情況下使用呢? MySQL手冊有詳細的介紹。
A. Next-Key 使用在被WHERE條件用到的索引上(準確的說是用來做Search的索引上)。
上面的例子中,Index(c2)上使用 Next-Key Lock.
B. Record Lock使用在沒有被WHERE條件使用的索引上。上面的例子中,簇索引上使用Record Lock.因此上面的UPDATE語句會同時在加Index(c2)的鍵1上加Next-Key,在主鍵1上加record鎖。當另一個session並發插入(2,5,2),(3,5,2)時可以成功,但是(2,2,2)時會被阻塞。
Next-Key And Record
測試時發現,SELECT…[FOR UPDATE |LOCKIN SHARE MODE]可能會導致全部記錄被鎖住。
當表很小時,SELECT會採用全表掃描的方法。在使用這種方法時,遍曆了所有的資料,因此所有資料都被鎖住了。儘管對不合格記錄調用了ha_innobase::unlock_row(),但是在Repeatable Read層級時不會被釋放。也許該算一個Bug.
C. A、B同時適用於SELECT…[FOR UPDATE | LOCK IN SHARE MODE], UPDATE、DELETE語句。
D. GAP鎖顯然也是使用在WHERE條件使用的索引上。和Next-Key不同的是,GAP鎖只加在上邊界(第一個大於合格記錄)上。而Next-Key加在所有合格記錄上。上面例子中的條件c2=2的記錄,需要在c2=3上加一個GAP鎖。
? 正向查詢時,InnoDB中實際上在邊界上加的是Next-Key鎖。 這可能是受實現的限制。
目前使用GAP情況有:
– Supremum記錄上始終是一個GAP鎖
– 反向查詢(ORDER BY DESC)時.
– 等值匹配一個確切的索引值時,對下一條記錄加GAP鎖。
– 等值匹配一個確切的索引值的首碼時,對下一條記錄加GAP鎖。。
E. INSERT時,通常不加鎖。只有當其他事務在插入點加了Gap或Next-key鎖需要等待時,才會建立一個插入意圖鎖。這個鎖是在waiting狀態。
- 隔離等級對Next-Key鎖的影響
A. Read Uncommitted和Read Committed時,不需要在間隙上加鎖,Nexk-Key變成Record鎖。
B. Repeatable Reads 和 Serializable時,通常情況下使用Next-key鎖。
有2中情況不需要對間隙加鎖:
– 查詢一個唯一的值,如 WHERE c1 = 1, c1 是主鍵或唯一鍵,並且查詢結果中不含NULL欄位。
– 當innodb_locks_unsafe_for_binlog被開啟。這裡還是有一些值得思考的問題:
- 從這個情況來看,UPDATE,DELETE時加間隙鎖完全是為了防止Master和Slave資料不一致。那麼不使用binlog時就沒有必要對DELETE, UPDATE加間隙鎖。
- Row Format Binlog時,不加間隙鎖是否會引起Master, Slave不一至。
- 即便設定了innodb_locks_unsafe_for_binlog,SELECT…[]是否可以不加間隙鎖。
判斷加什麼鎖的主要工作在row0sel.c:row_search_for_mysql()中。
02 – 延遲加鎖機制
如果一個表有很多的索引,那麼操作一個記錄時,豈不是要加很多鎖到不同的B-Tree上嗎?
先來看一個事務的狀態資訊:
CREATE TABLE t1(c1 INT KEY, c2 INT);
BEGIN;
INSERT INTO t1 VALUES(1, 1);
INSERT INTO t1 VALUES(2, 2);
SHOW ENGINE INNODB STATUS;
狀態資訊:
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 501, ACTIVE 0 sec
1 lock struct(s), heap size 376, 0 row lock(s), undo log entries 2
– 隱式鎖
Lock 是一種悲觀的順序化機制。它假設很可能發生衝突,因此在操作資料時,就加鎖。
如果衝突的可能性很小,多數的鎖都是不必要的。
Innodb 實現了一個延遲加鎖的機制,來減少加鎖的數量,在代碼中稱為隱式鎖(Implicit Lock)。
隱式鎖中有個重要的元素,事務ID(trx_id).隱式鎖的邏輯過程如下:
A. InnoDB的每條記錄中都一個隱含的trx_id欄位,這個欄位存在於簇索引的B+Tree中。
B. 在操作一條記錄前,首先根據記錄中的trx_id檢查該事務是否是活動的事務(未提交或復原).
如果是活動的事務,首先將隱式鎖轉換為顯式鎖(就是為該事務添加一個鎖)。
C. 檢查是否有鎖衝突,如果有衝突,建立鎖,並設定為waiting狀態。如果沒有衝突不加鎖,跳到E。
D. 等待加鎖成功,被喚醒,或者逾時。
E. 寫資料,並將自己的trx_id寫入trx_id欄位。Page Lock可以保證操作的正確性。
相關代碼:
A. lock_rec_convert_impl_to_expl()將隱式鎖轉換成顯示鎖。
B. 加鎖和測試行鎖衝突都用lock_rec_lock(),它的第一個參數表示是否是隱式鎖。所以要特別注意這個參數。如果為TRUE,在沒有衝突時並不會加鎖。
C. 測試行鎖的衝突的具體內容在lock_rec_has_wait()
D. 建立waiting鎖是lock_rec_enqueue_waiting()
E. 建立行鎖是lock_rec_add_to_queue()
– 隱式鎖的特點
A. 只有在很可能發生衝突時才加鎖,減少了鎖的數量。
B. 隱式鎖是針對被修改的B+Tree記錄,因此都是Record類型的鎖。不可能是Gap或Next-Key類型。
– 隱式鎖的使用
A. INSERT操作只加隱式鎖,不需要顯示加鎖。
B. UPDATE,DELETE在查詢時,直接對查詢用的Index和主鍵使用顯示鎖,其他索引上使用隱式鎖。
理論上說,可以對主鍵使用隱式鎖的。提前使用顯示鎖應該是為了減少死結的可能性。
INSERT,UPDATE,DELETE對B+Tree們的操作都是從主鍵的B+Tree開始,因此對主鍵加鎖可以有效阻止死結。
– Secondary Index上的隱式鎖
前邊說了, trx_id只存在於主鍵上,那麼輔助索引上如何來實現隱式索引呢?
顯然是要通過輔助索引中的主索引值,在主鍵B+Tree上進行二次尋找。這個開銷是很大的。
InnoDB對這個過程有一個最佳化:
A. 每個頁上有一個MAX_TRX_ID,每次修改輔助索引的記錄時,都會更新這個最大事務ID。
B. 當判斷是否要將隱式鎖變為顯式鎖時,先將頁面的max_trx_id和事務列表的最小trx_id比較。如果max_trx_id比事務列表的最小trx_id還小,那麼就不需要轉換為顯示鎖了。
代碼在lock_sec_rec_some_has_impl_off_kernel()中
/* Some transaction may have an implicit x-lock on the record onlyif the max trx id for the page >= min trx id for the trx list, ordatabase recovery is running. We do not write the changes of a page max trx id to the log, and therefore during recovery, this value for a page may be incorrect. */
if (page_get_max_trx_id(page) < trx_list_get_min_trx_id()
&& !recv_recovery_is_on()) {
return(NULL);
}
03 – 鎖的實現
– 鎖的存放
A. table->locks 存放一個表的所有表級鎖。
B. lock_sys->rec_hash存放所有表的行鎖。Hash值根據(spaceid, pageno)來計算。
C. trx->trx_locks存放事務的所有鎖,包括表級鎖和行級鎖。一個事務的所有鎖,在事務結束時,一起釋放。代碼在lock_release_off_kernel().如果有等待的鎖可以被授權,則會將等待的鎖,轉變為被授權的鎖,並喚醒相應的事務。
– 行鎖的唯一識別
第一印象想到的是,用每行記錄的索引值來做行鎖的唯一識別.但是索引值佔用空間比較大。
InnoDB使用Page NO.+Heap NO.來做行鎖的唯一識別。我們可以將Heap no.理解為頁面上的一個自增數值。每條物理記錄在被建立時,都會分配一個唯一的heap no.
A. 索引值可以理解為一個邏輯值,page no. + heap no. 是物理的。
B. 物理的雖然佔用空間小,但是處理要複雜一些。如:在分裂一個B+Tree頁面時,一半的記錄要移到新的頁面中,因此要對存在的鎖進行遷移。
鎖移動的d函數有:lock_move_reorganize_page(), lock_move_rec_list_start(),
lock_move_rec_list_end().
在刪除和插入資料時,也要進行GAP鎖的繼承。lock_rec_inherit_to_gap()
lock_rec_inherit_to_gap_if_gap_lock().
– 死結(Deadlock)
A. 逾時機制。當要加的鎖和其他鎖衝突時,添加一個waiting鎖,並且返回DB_LOCK_WAIT錯誤。
row_mysql_handle_error調用srv_suspend_mysql_thread來掛起一個線程。
B. 死結檢測檢測機制。每當建立waiting鎖,都要調用lock_deadlock_occurs()進行死結的檢測。
死結檢測方法是Waits-For Graph.在lock_deadlock_recursive()中實現。
當發現死結後要選擇其中的一個事務,將其復原,來解除死結。選擇哪一個交易回復能?
– 如果一個事務修改了non-transactional表(如MyISAM表,修改不能復原),另一個表沒有。
則沒有修改non-transactional的會被復原。
– 如果2個事務都修改了non-transactional表或者都沒有。則比較2個事務修改的記錄數和加的鎖數量。總和小的事務會被復原。trx_weight_ge()實現這個邏輯。
MySQL資料庫InnoDB儲存引擎中的鎖機制--轉載