MySQL學習筆記:InnoDB中的7種鎖
InnoDB的七種鎖
先從一個有意思的案例,引出了鎖的話題。
假設有資料表:
t(id int PK, name);
目前的記錄是:
10, shenjian
20, zhangsan
30, lisi
事務A先執行,並且處於未提交狀態:
delete from t where id=40;
事務A想要刪除一條不存在的記錄。
事務B後執行:
insert into t values(40, ‘c’);
事務B想要插入一條主鍵不衝突的記錄。
問題1:事務B是否阻塞?
問題2:如果事務B阻塞,鎖如何加在一條不存在的記錄上呢?
問題3:事務的隔離級別,索引型別,是否對問題1和問題2有影響呢?
接下來詳細的介紹InnoDB核心中的七種鎖。
自增鎖
一,案例說明
MySQL,InnoDB,預設的隔離級別(RR),假設有資料表:
t(id AUTO_INCREMENT, name);
資料表中有資料:
1, shenjian
2, zhangsan
3, lisi
事務A先執行,還未提交:
insert into t(name) values(xxx);
事務B後執行:
insert into t(name) values(ooo);
問:事務B會不會被阻塞?
二,案例分析
InnoDB在RR隔離級別下,能解決幻讀問題,上面這個案例中:
(1)事務A先執行insert,會得到一條(4, xxx)的記錄,由於是自增列,故不用顯示指定id為4,InnoDB會自動增長,注意此時事務並未提交;
(2)事務B後執行insert,假設不會被阻塞,那會得到一條(5, ooo)的記錄;
此時,並未有什麼不妥,但如果,
(3)事務A繼續insert:
insert into t(name) values(xxoo);
會得到一條(6, xxoo)的記錄。
(4)事務A再select:
select * from t where id>3;
得到的結果是:
4, xxx
6, xxoo
畫外音:不可能查詢到5的記錄,再RR的隔離級別下,不可能讀取到還未提交事務生成的資料。
咦,這對於事務A來說,就很奇怪了,對於AUTO_INCREMENT的列,連續插入了兩條記錄,一條是4,接下來一條變成了6,就像莫名其妙的幻影。
三,自增鎖(Auto-inc Locks)
自增鎖是一種特殊的表級別鎖(table-level lock),專門針對事務插入AUTO_INCREMENT型別的列。最簡單的情況,如果一個事務正在往表中插入記錄,所有其他事務的插入必須等待,以便第一個事務插入的行,是連續的主鍵值。
畫外音:官網是這麼說的
An AUTO-INC lock is a special table-level lock taken by transactions inserting into tables with AUTO_INCREMENT columns. In the simplest case, if one transaction is inserting values into the table, any other transactions must wait to do their own inserts into that table, so that rows inserted by the first transaction receive consecutive primary key values.
與此同時,InnoDB提供了innodb_autoinc_lock_mode配置,可以調節與改變該鎖的模式與行為。
共享/排它鎖(Shared and Exclusive Locks)
在InnoDB裡實現了標準的行級鎖(row-level locking),共享/排它鎖:
(1)事務拿到某一行記錄的共享S鎖,才可以讀取這一行;
(2)事務拿到某一行記錄的排它X鎖,才可以修改或者刪除這一行;
其相容互斥表如下:
S X
S 相容 互斥
X 互斥 互斥
即:
(1)多個事務可以拿到一把S鎖,讀讀可以並行;
(2)而只有一個事務可以拿到X鎖,寫寫/讀寫必須互斥;
共享/排它鎖的潛在問題是,不能充分的並行,解決思路是資料多版本。
意向鎖(Intention Locks)
InnoDB支援多粒度鎖(multiple granularity locking),它允許行級鎖與表級鎖共存,實際應用中,InnoDB使用的是意向鎖。
意向鎖是指,未來的某個時刻,事務可能要加共享/排它鎖了,先提前宣告一個意向。
意向鎖有這樣一些特點:
(1)首先,意向鎖,是一個表級別的鎖(table-level locking);
(2)意向鎖分為:
意向共享鎖(intention shared lock, IS),它預示著,事務有意向對錶中的某些行加共享S鎖
意向排它鎖(intention exclusive lock, IX),它預示著,事務有意向對錶中的某些行加排它X鎖
舉個例子:
select ... lock in share mode,要設定IS鎖;
select ... for update,要設定IX鎖;
(3)意向鎖協議(intention locking protocol)並不複雜:
事務要獲得某些行的S鎖,必須先獲得表的IS鎖
事務要獲得某些行的X鎖,必須先獲得表的IX鎖
(4)由於意向鎖僅僅表明意向,它其實是比較弱的鎖,意向鎖之間並不相互互斥,而是可以並行,其相容互斥表如下:
IS IX
IS 相容 相容
IX 相容 相容
(5)額,既然意向鎖之間都相互相容,那其意義在哪裡呢?它會與共享鎖/排它鎖互斥,其相容互斥表如下:
S X
IS 相容 互斥
IX 互斥 互斥
畫外音:排它鎖是很強的鎖,不與其他型別的鎖相容。這也很好理解,修改和刪除某一行的時候,必須獲得強鎖,禁止這一行上的其他併發,以保障資料的一致性。
插入意向鎖(Insert Intention Locks)
對已有資料行的修改與刪除,必須加強互斥鎖X鎖,那對於資料的插入,是否還需要加這麼強的鎖,來實施互斥呢?插入意向鎖,孕育而生。
插入意向鎖,是間隙鎖(Gap Locks)的一種(所以,也是實施在索引上的),它是專門針對insert操作的。
畫外音:有點尷尬,間隙鎖下一篇文章才會介紹,暫且理解為,它是一種實施在索引上,鎖定索引某個區間範圍的鎖。
它的玩法是:
多個事務,在同一個索引,同一個範圍區間插入記錄時,如果插入的位置不衝突,不會阻塞彼此。
畫外音:官網的說法是
Insert Intention Lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap.
這樣,之前挖坑的例子,就能夠解答了。
在MySQL,InnoDB,RR下:
t(id unique PK, name);
資料表中有資料:
10, shenjian
20, zhangsan
30, lisi
事務A先執行,在10與20兩條記錄中插入了一行,還未提交:
insert into t values(11, xxx);
事務B後執行,也在10與20兩條記錄中插入了一行:
insert into t values(12, ooo);
(1)會使用什麼鎖?
(2)事務B會不會被阻塞呢?
回答:雖然事務隔離級別是RR,雖然是同一個索引,雖然是同一個區間,但插入的記錄並不衝突,故這裡:
使用的是插入意向鎖
並不會阻塞事務B
思路總結
(1)InnoDB使用共享鎖,可以提高讀讀併發;
(2)為了保證資料強一致,InnoDB使用強互斥鎖,保證同一行記錄修改與刪除的序列性;
(3)InnoDB使用插入意向鎖,可以提高插入併發;
InnoDB的索引(用於引出後三種鎖
InnoDB的索引有兩類索引,聚集索引(Clustered Index)與普通索引(Secondary Index)。
InnoDB的每一個表都會有聚集索引:
(1)如果表定義了PK,則PK就是聚集索引;
(2)如果表沒有定義PK,則第一個非空unique列是聚集索引;
(3)否則,InnoDB會建立一個隱藏的row-id作為聚集索引;
為了方便說明,後文都將以PK說明。
索引的結構是B+樹,這裡不展開B+樹的細節,說幾個結論:
(1)在索引結構中,非葉子節點儲存key,葉子節點儲存value;
(2)聚集索引,葉子節點儲存行記錄(row);
畫外音:所以,InnoDB索引和記錄是儲存在一起的,而MyISAM的索引和記錄是分開儲存的。
(3)普通索引,葉子節點儲存了PK的值;
畫外音:
所以,InnoDB的普通索引,實際上會掃描兩遍:
第一遍,由普通索引找到PK;
第二遍,由PK找到行記錄;
索引結構,InnoDB/MyISAM的索引結構,如果大家感興趣,未來撰文詳述。
舉個例子,假設有InnoDB表:
t(id PK, name KEY, sex, flag);
表中有四條記錄:
1, shenjian, m, A
3, zhangsan, m, A
5, lisi, m, A
9, wangwu, f, B
以看到:
(1)第一幅圖,id PK的聚集索引,葉子儲存了所有的行記錄;
(2)第二幅圖,name上的普通索引,葉子儲存了PK的值;
對於:
select * from t where name=’shenjian’;
(1)會先在name普通索引上查詢到PK=1;
(2)再在聚集索引衫查詢到(1,shenjian, m, A)的行記錄;
下文簡單介紹InnoDB七種鎖中的剩下三種:
記錄鎖(Record Locks)
間隙鎖(Gap Locks)
臨鍵鎖(Next-Key Locks)
為了方便講述,如無特殊說明,後文中,預設的事務隔離級別為可重複讀(Repeated Read, RR)。
記錄鎖(Record Locks)
記錄鎖,它封鎖索引記錄,例如:
select * from t where id=1 for update;
它會在id=1的索引記錄上加鎖,以阻止其他事務插入,更新,刪除id=1的這一行。
需要說明的是:
select * from t where id=1;
則是快照讀(SnapShot Read),它並不加鎖,具體在《InnoDB為什麼併發高,讀取快?》中做了詳細闡述。
間隙鎖(Gap Locks)
間隙鎖,它封鎖索引記錄中的間隔,或者第一條索引記錄之前的範圍,又或者最後一條索引記錄之後的範圍。
依然是上面的例子,InnoDB,RR:
t(id PK, name KEY, sex, flag);
表中有四條記錄:
1, shenjian, m, A
3, zhangsan, m, A
5, lisi, m, A
9, wangwu, f, B
這個SQL語句
select * from t
where id between 8 and 15
for update;
會封鎖區間,以阻止其他事務id=10的記錄插入。
畫外音:
為什麼要阻止id=10的記錄插入?
如果能夠插入成功,頭一個事務執行相同的SQL語句,會發現結果集多出了一條記錄,即幻影資料。
間隙鎖的主要目的,就是為了防止其他事務在間隔中插入資料,以導致“不可重複讀”。
如果把事務的隔離級別降級為讀提交(Read Committed, RC),間隙鎖則會自動失效。
臨鍵鎖(Next-Key Locks)
臨鍵鎖,是記錄鎖與間隙鎖的組合,它的封鎖範圍,既包含索引記錄,又包含索引區間。
更具體的,臨鍵鎖會封鎖索引記錄本身,以及索引記錄之前的區間。
如果一個會話佔有了索引記錄R的共享/排他鎖,其他會話不能立刻在R之前的區間插入新的索引記錄。
畫外音:原文是說
If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.
依然是上面的例子,InnoDB,RR:
t(id PK, name KEY, sex, flag);
表中有四條記錄:
1, shenjian, m, A
3, zhangsan, m, A
5, lisi, m, A
9, wangwu, f, B
PK上潛在的臨鍵鎖為:
(-infinity, 1]
(1, 3]
(3, 5]
(5, 9]
(9, +infinity]
臨鍵鎖的主要目的,也是為了避免幻讀(Phantom Read)。如果把事務的隔離級別降級為RC,臨鍵鎖則也會失效。
畫外音:關於事務的隔離級別,以及幻讀,之前的文章一直沒有展開說明,如果大家感興趣,後文詳述。
今天的內容,主要對InnoDB的索引,以及三種鎖的概念做了介紹。場景與例子,也都是最簡單的場景與最簡單的例子。
InnoDB的鎖,與索引型別,事務的隔離級別相關,更多更復雜更有趣的案例,後續和大家介紹。
總結
(1)InnoDB的索引與行記錄儲存在一起,這一點和MyISAM不一樣;
(2)InnoDB的聚集索引儲存行記錄,普通索引儲存PK,所以普通索引要查詢兩次;
(3)記錄鎖鎖定索引記錄;
(4)間隙鎖鎖定間隔,防止間隔中被其他事務插入;
(5)臨鍵鎖鎖定索引記錄+間隔,防止幻讀;
知識點來源@58沈劍,感謝作者。公眾號:架構師之路。