所在文集:數據庫
本文的內容參考了:
- 架構師之路 - 挖坑,InnoDB的七種鎖
- 架構師之路 - 插入InnoDB自增列,居然是表鎖?
- 架構師之路 - InnoDB并發插入,居然使用意向鎖?
- 架構師之路 - InnoDB,select為啥會阻塞insert?
- 架構師之路 - 別廢話,各種SQL到底加了什么鎖?
- 架構師之路 - 超贊,InnoDB調試死鎖的方法!
下面會涉及到 MySQL 數據庫隔離級別和索引,請先參見:
自增鎖
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會不會被阻塞? 答案是會阻塞,分析如下:
- 事務 A 先執行
insert
,會得到一條(4, xxx)
的記錄,由于是自增列,InnoDB 會自動增長,注意此時事務并未提交; - 事務 B 后執行
insert
,假設不會被阻塞,那會得到一條(5, ooo)
的記錄;
此時,并未有什么不妥,但如果,事務 A 繼續 insert
:insert into t(name) values(xxoo);
會得到一條 (6, xxoo)
的記錄。
事務 A 再 select
:select * from t where id>3;
得到的結果是:
4, xxx
6, xxoo
注意:不可能查詢到 (5, ooo)
的記錄,因為在 RR 的隔離級別下,不可能讀取到還未提交事務生成的數據。
這對于事務 A 來說,就很奇怪了,對于 AUTO_INCREMENT
的列,連續插入了兩條記錄,一條是 4,接下來一條變成了 6,就像莫名其妙的幻影。
自增鎖是一種特殊的表級別鎖(table-level lock),專門針對事務插入 AUTO_INCREMENT
類型的列。最簡單的情況,如果一個事務正在往表中插入記錄,所有其他事務的插入必須等待,以便第一個事務插入的行,是連續的主鍵值。
共享/排他鎖(Shared and Exclusive Locks)
共享鎖(S鎖)和排他鎖(X鎖)是行級別的鎖(row-level locking):
- 事務拿到某一行記錄的共享S鎖,才可以讀取這一行;
- 多個事務可以拿到一把共享S鎖,讀讀可以并行
- 事務拿到某一行記錄的排它X鎖,才可以修改或者刪除這一行;
- 只有一個事務可以拿到排它X鎖,寫寫/讀寫必須互斥;
共享/排它鎖的潛在問題是,不能充分的并行,解決思路是數據多版本。參見:MySQL InnoDB 并發控制,事務的實現 學習筆記
意向鎖(Intention Locks)
意向鎖是指,未來的某個時刻,事務可能要加共享/排它鎖了,先提前聲明一個意向。意向鎖有這樣一些特點:
- 首先,意向鎖,是一個表級別的鎖(table-level locking);
- 意向鎖分為:
- 意向共享鎖(intention shared lock, IS)例如:
select ... lock in share mode
要設置意向共享鎖; - 意向排它鎖(intention exclusive lock, IX)例如:
select ... for update
要設置意向排它鎖;
- 意向共享鎖(intention shared lock, IS)例如:
- 意向鎖協議(intention locking protocol)并不復雜:
- 事務要獲得某些行的 S 鎖,必須先獲得表的 IS 鎖
- 事務要獲得某些行的 X 鎖,必須先獲得表的 IX 鎖
- 由于意向鎖僅僅表明意向,它其實是比較弱的鎖,意向鎖之間并不相互互斥,而是可以并行
插入意向鎖(Insert Intention Locks)
對已有數據行的修改 update
與刪除 delete
,必須加排他鎖(X鎖),那對于數據的插入 insert
,是否還需要加這么強的鎖,來實施互斥呢?
插入意向鎖,是間隙鎖(Gap Locks)的一種(所以,也是實施在索引上的),它是專門針對 insert
操作的:
多個事務,在同一個索引,同一個范圍區間插入記錄時,如果插入的位置不沖突,不會阻塞彼此。
回到上面自增鎖時所用的插入的例子。如果主鍵不是自增的 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);
問:會使用什么鎖?使用的是插入意向鎖。
問:事務B會不會被阻塞呢?雖然事務隔離級別是 RR,雖然是同一個索引,雖然是同一個區間,但插入的記錄并不沖突,故這里并不會阻塞事務 B。
思路總結:
- InnoDB 使用共享鎖,可以提高讀讀并發;
- 為了保證數據強一致,InnoDB 使用強互斥鎖,保證同一行記錄修改與刪除的串行性;
- InnoDB 使用插入意向鎖,可以提高插入并發;
記錄鎖(Record Locks)
記錄鎖封鎖索引記錄,例如:
select * from t where id=1 for update;
它會在 id=1
的索引記錄上加鎖,以阻止其他事務插入,更新,刪除 id=1
的這一行。
需要說明的是:
select * from t where id=1;
則是快照讀(SnapShot Read),它并不加鎖。
間隙鎖(Gap Locks)
間隙鎖封鎖索引記錄中的間隔,或者第一條索引記錄之前的范圍,又或者最后一條索引記錄之后的范圍。
t(id PK, name KEY);
表中有四條記錄:
1, shenjian
3, zhangsan
5, lisi
9, wangwu
SQL 語句:select * from t where id between 8 and 15 for update;
會封鎖區間,以阻止其他事務 id=10
的記錄插入。
為什么要阻止 id=10
的記錄插入?如果能夠插入成功,頭一個事務執行相同的 SQL 語句,會發現結果集多出了一條記錄,即幻影數據。(即我們采用 MySQL InnoDB 的 可重復讀 RR 隔離級別)
如果把事務的隔離級別降級為讀提交(Read Committed, RC),間隙鎖則會自動失效。
臨鍵鎖(Next-Key Locks)
臨鍵鎖,是記錄鎖與間隙鎖的組合,它的封鎖范圍,既包含索引記錄,又包含索引區間。
更具體的,臨鍵鎖會封鎖索引記錄本身,以及索引記錄之前的區間。
t(id PK, name KEY);
表中有四條記錄:
1, shenjian
3, zhangsan
5, lisi
9, wangwu
主鍵上潛在的臨鍵鎖為:
(-infinity, 1]
(1, 3]
(3, 5]
(5, 9]
(9, +infinity]
臨鍵鎖的主要目的,也是為了避免幻讀(Phantom Read)。如果把事務的隔離級別降級為RC,臨鍵鎖則也會失效。
各種SQL到底加了什么鎖
普通 select
- 在讀未提交(Read Uncommitted),讀提交(Read Committed, RC),可重復讀(Repeated Read, RR)這三種事務隔離級別下,普通
selec
使用快照讀(snpashot read),不加鎖,并發非常高; - 在串行化(Serializable)這種事務的隔離級別下,普通
select
會升級為select ... in share mode;
加鎖 select
select ... for update
select ... in share mode
如果,在唯一索引(unique index)上使用唯一的查詢條件(unique search condition),會使用記錄鎖(record lock),而不會封鎖記錄之間的間隔,即不會使用間隙鎖(gap lock)與臨鍵鎖(next-key lock);
假設有 InnoDB 表:t(id PK, name);
1, shenjian
2, zhangsan
3, lisi
SQL 語句:select * from t where id=1 for update;
只會封鎖記錄,而不會封鎖區間。
其他的查詢條件和索引條件,InnoDB 會封鎖被掃描的索引范圍,并使用間隙鎖與臨鍵鎖,避免索引范圍區間插入記錄。
update 與 delete
和加鎖 select
類似,如果在唯一索引上使用唯一的查詢條件來 update/delete
,例如:
update t set name=xxx where id=1;
也只加記錄鎖;
否則,符合查詢條件的索引記錄之前,都會加排他臨鍵鎖(exclusive next-key lock),來封鎖索引記錄與之前的區間;
尤其需要特殊說明的是,如果 update
的是聚集索引(clustered index)記錄,則對應的普通索引(secondary index)記錄也會被隱式加鎖,這是由 InnoDB 索引的實現機制決定的:普通索引存儲 PK 的值,檢索普通索引本質上要二次掃描聚集索引。
insert
同樣是寫操作,insert
和 update
與 delete
不同,它會用排它鎖封鎖被插入的索引記錄,而不會封鎖記錄之前的范圍。
同時,會在插入區間加插入意向鎖(insert intention lock),但這個并不會真正封鎖區間,也不會阻止相同區間的不同 KEY 插入。
InnoDB 調試死鎖
InnoDB 的行鎖都是實現在索引上的,實驗可以使用主鍵,建表時設定為 InnoDB 引擎:
create table t (
id int(10) primary key
)engine=innodb;
插入一些實驗數據:
start transaction;
insert into t values(1);
insert into t values(3);
insert into t values(10);
commit;
實驗一,間隙鎖互斥
開啟區間鎖,RR 的隔離級別下,上例會有四個區間:
(-infinity, 1)
(1, 3)
(3, 10)
(10, infinity)
事務 A 刪除某個區間內的一條不存在記錄,獲取到共享間隙鎖,會阻止其他事務 B 在相應的區間插入數據,因為插入需要獲取排他間隙鎖。
session A:
set session autocommit=0;
start transaction;
delete from t where id=5;
session B:
set session autocommit=0;
start transaction;
insert into t values(0);
insert into t values(2);
insert into t values(12);
insert into t values(7);
事務 B 插入的值:0
, 2
, 12
都不在 (3, 10)
區間內,能夠成功插入,而 7
在 (3, 10)
這個區間內,會阻塞。
可以使用:show engine innodb status;
來查看鎖的情況。
如果事務 A 提交或者回滾,事務 B 就能夠獲得相應的鎖,以繼續執行。
如果事務 A 一直不提交,事務 B 會一直等待,直到超時。
實驗二,共享排他鎖死鎖
事務 A 先執行:
set session autocommit=0;
start transaction;
insert into t values(7);
事務 B 后執行:
set session autocommit=0;
start transaction;
insert into t values(7);
事務 C 最后執行:
set session autocommit=0;
start transaction;
insert into t values(7);
三個事務都試圖往表中插入一條為 7
的記錄:
- A 先執行,插入成功,并獲取
id=7
的排他鎖; - B 后執行,需要進行PK校驗,故需要先獲取
id=7
的共享鎖,阻塞; - C 后執行,也需要進行PK校驗,也要先獲取
id=7
的共享鎖,也阻塞;
如果此時,事務 A 執行:rollback;
釋放 id=7
排他鎖。
則 B,C 會繼續進行主鍵校驗:
- B 會獲取到
id=7
共享鎖,主鍵未互斥; - C 也會獲取到
id=7
共享鎖,主鍵未互斥;
B 和 C 要想插入成功,必須獲得 id=7
的排他鎖,但由于雙方都已經獲取到 id=7
的共享鎖,它們都無法獲取到彼此的排他鎖,死鎖就出現了。
當然,InnoDB有死鎖檢測機制,B 和 C 中的一個事務會插入成功,另一個事務會自動放棄。
共享排他鎖,在并發量插入相同記錄的情況下會出現,相應的案例比較容易分析。
實驗三,并發間隙鎖的死鎖
SQL 執行序列如下:
A:set session autocommit=0;
A:start transaction;
A:delete from t where id=6;
B:set session autocommit=0;
B:start transaction;
B:delete from t where id=7;
A:insert into t values(5);
B:insert into t values(8);
- A 執行
delete
后,會獲得(3, 10)
的共享間隙鎖。 - B 執行
delete
后,也會獲得(3, 10)
的共享間隙鎖。 - A 執行
insert
后,希望獲得(3, 10)
的排他間隙鎖,于是會阻塞。 - B 執行
insert
后,也希望獲得(3, 10)
的排他間隙鎖,于是死鎖出現。
檢測到死鎖后,事務 B 自動回滾了,事務 A 將會執行成功。