1. 程式人生 > 實用技巧 >關於 InnoDB 鎖的超全總結

關於 InnoDB 鎖的超全總結

關於 InnoDB 鎖的超全總結

轉載自:https://www.cnblogs.com/michael9/p/12443975.html

目錄


幾個月之前,開始深入學習 MySQL 。說起資料庫,併發控制是其中很重要的一部分。於是,就這樣開起了 MySQL 鎖的學習,隨著學習的深入,發現想要更好的理解鎖,需要了解 MySQL 事務,資料底層的儲存方式,MySQL 的執行流程,特別是索引的選擇等。

在學習期間,查找了不少資料,現根據個人的理解總結下來,方便日後複習。

Top

InnoDB 鎖一覽

先從 MySQL 官網的鎖介紹開始,來逐一認識下這些讓我們夜不能寐的小王八蛋:

Shared and Exclusive Locks

這二位正式稱呼呢,就是共享鎖和排他鎖,其實就是我們常說的讀鎖和寫鎖。它們之間的互斥規則,想必都清楚,就不贅述了。但有一點需要注意,共享鎖和排他鎖是標準的實現行級別的鎖。舉例來說,當給 select 語句應用 lock in share mode 或者 for update,或者更新某條記錄時,加的都是行級別的鎖。

與行級別的共享鎖和排他鎖類似的,還有表級別的共享鎖和排他鎖。如 LOCK TABLES ... WRITE/READ

等命令,實現的就是表級鎖。

Intention Locks

在 InnoDB 中是支援多粒度的鎖共存的,比如表鎖和行鎖。而 Intention Locks - 意向鎖,就是表級鎖。和行級鎖一樣,意向鎖分為 intention shared lock (IS) 和 intention exclusive lock (IX) . 但有趣的是,IS 和 IX 之間並不互斥,也就是說可以同時給不同的事務加上 IS 和 IX. 相容性如下:

這時就產生疑問了,那它倆存在的意義是什麼?作用就是,和共享鎖和排他鎖互斥。注意下,這裡指的是表級別的共享鎖和排他鎖,和行級別沒有關係!

官網中給了這樣一段解釋:

The main purpose of intention locks is to show that someone is locking a row, or going to lock a row in the table.

意向鎖的目的就是表明有事務正在或者將要鎖住某個表中的行。想象這樣一個場景,我們使用 select * from t where id=0 for update; 將 id=0 這行加上了寫鎖。假設同時,一個新的事務想要發起 LOCK TABLES ... WRITE 鎖表的操作,這時如果沒有意向鎖的話,就需要去一行行檢測是否所在表中的某行是否存在寫鎖,從而引發衝突,效率太低。相反有意向鎖的話,在發起 lock in share mode 或者 for update 前就會自動加上意向鎖,這樣檢測起來就方便多了。

在實際中,手動鎖表的情況並不常見,所以意向鎖並不常用。特別是之後 MySQL 引入了 MDL 鎖,解決了 DML 和 DDL 衝突的問題,意向鎖就更不被提起來了。

Record Locks

record lock ,就是常說的行鎖。InnoDB 中,表都以索引的形式存在,每一個索引對應一顆 B+ 樹,這裡的行鎖鎖的就是 B+ 中的索引記錄。之前提到的共享鎖和排他鎖,就是將鎖加在這裡。

Gap Locks

Gap Locks, 間隙鎖鎖住的是索引記錄間的空隙,是為了解決幻讀問題被引入的。有一點需要注意,間隙鎖和間隙鎖本身之間並不衝突,僅僅和插入這個操作發生衝突。

Next-Key lock

next-key lock 是行鎖(Record)和間隙鎖的並集在 RR 級別下,InnoDB 使用 next-key 鎖進行樹搜尋和索引掃描。記住這句話,加鎖的基本單位是 next-key lock.

Top

加鎖規則

該加鎖原則由林曉斌老師刷程式碼後總結,符合的版本如下:

  1. MySQL 版本:5.x - 5.7.24, 8.0 - 8.0.13. 我是 5.7.27 也未發現問題。

規則包括:兩個“原則”、兩個“優化”和一個“bug”。

  1. 原則1:加鎖的基本單位是 next-key lock。next-key lock 是前開後閉區間。
  2. 原則2:查詢過程中訪問到的物件才會加鎖。
  3. 優化1:索引上的等值查詢,給唯一索引加鎖的時候,next-key lock 退化為行鎖。
  4. 優化2:索引上的等值查詢,向右遍歷時且最後一個值不滿足等值條件的時候,next-key lock 退化為間隙鎖。
  5. 一個 bug:唯一索引上的範圍查詢會訪問到不滿足條件的第一個值為止。

解釋下容易理解錯誤的地方:

  1. 對優化 2 的說明:

    從等值查詢的值開始,向右遍歷到第一個不滿足等值條件記錄結束,然後將不滿足條件記錄的 next-key 退化為間隙鎖。

  2. 等值查詢和遍歷有什麼關係?

    在分析加鎖行為時,一定要從索引的資料結構開始。通過樹搜尋的方式定位索引記錄時,用的是"等值查詢",而遍歷對應的是在記錄上向前或向後掃描的過程。

Top

應用場景

表結構如下:

CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

場景1:主鍵索引等值間歇鎖

Session ASession BSession C
begin;
update t set d=d+1 where id=7;
insert into t values(8,8,8);
被阻塞 update t set d=d+1 where id=10;
查詢正常

其中 id 列為主鍵索引,並且 id=7 的行並不存在。

對於 Session A 來說:

  1. 根據原則1,加鎖的單位是 next-key, 因為 id=7 在 5 - 10 間,next-key 預設是左開右閉。所以範圍是 (5,10].
  2. 根據優化2,但因為 id=7 是等值查詢,到 id=10 結束。next-key 退化成間隙鎖 (5,10).

對於 Session B 來說:

  • 插入操作與間隙鎖衝突,所以失敗。

對於 Session C 來說:

  1. 根據原則1,next-key 加鎖 (5,10].
  2. 根據優化1:給唯一索引加鎖時,退化成行鎖。範圍變為:id=10 的行鎖
  3. Session C 和 Session A (5,10) 並不起衝突,所以成功。

這裡可以看出,行鎖和間隙鎖都是有 next-key 鎖滿足一定後條件後轉換的,加鎖的預設單位是 next-key.

場景2:非唯一索引等值鎖

Session ASession BSession C
begin;
select id from t where c=5 lock in share mode;
update t set d=d+1 where id=5;
查詢正常 insert into t values(7,7,7);
被阻塞

關注幾點:c為非唯一索引,查詢的欄位僅有 id,lock in share mode 給滿足條件的行加上讀鎖。

Session A:

  1. c=5,等值查詢且值存在。先加 next-key 鎖,範圍為 (0,5].
  2. 由於 c 是普通索引,因此僅訪問 c=5 這一條記錄不會停止,會繼續向右遍歷,到 10 結束。根據原則2,這時會給 id=10 加 next-key (5,10].
  3. 但 id=10 同時滿足優化2,退化成間隙鎖 (5,10).
  4. 根據原則2,該查詢使用覆蓋索引,可以直接得到 id 的值,主鍵索引未被訪問到,不加鎖。

Session B:

  1. 根據原則1 和優化1,給 id=10 的主鍵索引加行鎖,並不衝突,修改成功。

Session C:

  1. 由於 Session A 已經對索引 c 中 (5,10) 的間隙加鎖,與插入 c=7 衝突, 所以被阻塞。

可以看出,加鎖其實是在索引上,並且只加在訪問到的記錄上,如果想要在 lock in share mode 下避免資料被更新,需要引入覆蓋索引不能包含的欄位。

假設將 Session A 的語句改成 select id from t where c=5 for update;, for update 表示可能當前事務要更新資料,所以也會給滿足的條件的主鍵索引加鎖。這時 Session B 就會被阻塞了。

場景3:非唯一索引等值鎖-鎖主鍵

Session ASession B
begin;
select id from t where c=5 lock in share mode;
insert into t values(9,10,7);
被阻塞

和場景 2 很相似,該例主要是為了更好的說明間隙的概念。

Session A 的加鎖範圍不變,給索引 C 加了 (0,5] 和 (5,10) 的行鎖。需要知道的是,非唯一索引形成的 key,需要包含主鍵 id,用於保證唯一性,畫個圖如下。

由圖中可知,由於非唯一索引存在主鍵id,並且按照 B+ 樹的排序規則,不光 c 的值在加鎖範圍內不能被更改和插入,對應 id 的範圍也不能被更改。怎麼理解這句話呢,上個例子不是說,主鍵索引不會被加鎖,怎麼這裡的主鍵 id 又被鎖了呢?

首先主鍵索引是另外一顆B+樹確實沒有被鎖,但這裡由於 C 是非唯一索引,形成的B+樹需要將主鍵索引的 Id 包含進來,並在按照先 c 欄位,後 id 欄位進行排序。這樣,在給 c 欄位加行鎖時,對應的 id 也同時加了行鎖。

上面例子中,Session2 更新成功是因為修改的是 d 地段,並沒有更新 id 的值,所以成功了。而這裡想要插入的 (id=6, c=10), 雖然 c=10 沒有鎖,但 id=6 卻在鎖的範圍內,所以這裡就被阻塞了。同樣插入 (id=100,c=6) 也會被阻塞。

也就說,對於非唯一索引,考慮加鎖範圍時要考慮到主鍵 Id 的情況。

場景4:主鍵索引範圍鎖

Session ASession BSession C
begin;
select * from t where id>=10 and id <11 for update;
insert into t values(8,8,8);
正常
insert into t values(13,13,13);
被阻塞
update t set d=d+1 where id=15;
被阻塞

Session A:

  1. 先找到 id=10 行,屬於等值查詢。根據原則1,優化1,範圍是 id=10 這行。
  2. 向後遍歷,屬於範圍查詢,找到 id=15 這行,根據原則2,範圍是 (10,15]

Session B:

  1. 插入 (8,8,8) 可以,(13,13,13) 就不行了。

Session C:

  1. id=15 同樣在鎖的範圍內,所以也被阻塞。

場景5:非唯一索引範圍鎖

Session ASession BSession C
begin;
select * from t where c>=10 and c <11 for update;
insert into t values(8,8,8);
被阻塞
insert into t values(13,13,13);
被阻塞
update t set d=d+1 where c=15;
被阻塞

Session A:

  1. 由於 c 是非唯一索引,索引對於 c=10 等值查詢來說,根據原則1,加鎖範圍為 (5,10].
  2. 向右遍歷,範圍查詢,加鎖範圍為 (10,15].

Session B:

  1. (8,8,8) 和 (13,13,13) 都衝突,所以被阻塞。

Session C:

  1. c=5 也被鎖住,也會被阻塞。

場景6:唯一索引範圍鎖 bug

Session ASession BSession C
begin;
select * from t where id>10 and id <=15 for update;
update t set d=d+1 where id=20;
被阻塞
insert into t values(16,16,16);
被阻塞

Session A:

  1. 由於開始找大於 10 的過程中是第一次是範圍查詢,所以沒有優化原則。加 (10,15].
  2. 有一個特殊的地方,理論上說由於 id 是唯一主鍵,找到 id=15 就應該停下來了,但實際沒有。根據 bug 原則,會繼續掃描第一個不滿足的值為止,接著找到 id=20,因為是範圍查詢,沒有優化原則,繼續加鎖 (15,20].

這個 bug 在 8.0.18 後已經修復了

對於 Session B 和 Session C 均和加鎖的範圍衝突。

場景7:非唯一索引 delete

Session ASession BSession C
begin;
delete from t where c=10;
insert into t values(12,12,12)
被阻塞
update t set d=d+1 where c=15;
成功

delete 後加 where 語句和 select * for update 語句的加鎖邏輯類似。

Session A:

  1. 根據原則1,加 (5,10] 的 next-key.
  2. 向右遍歷,根據優化2,加 (10,15) 的間歇鎖。

場景8:非唯一索引 limit

Session ASession B
begin;
delete from t where c=10 limit 1;
insert into t values(12,12,12)
正常

雖然 c=10 只有一條記錄,但和場景7 的加鎖範圍不同。

Session A:

  1. 根據原則1,加 (5,10] 的 next-key.
  2. 因為加了 limit 1,所以找到一條就可以了,不需要繼續遍歷,也就是說不在加鎖。

所以對於 session B 來說,就不在阻塞。

場景9:next-key 引發死鎖

Session ASession B
begin;
select id from t where c=10 lock in share mode;
update t set d=d+1 where c=10;
被阻塞
insert into t values(8,8,8)
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
  1. Session A 第一句 加 (5,10] next-key. 和 (10,15) 的 間隙鎖。
  2. Session B,和 Session A 想要的加鎖範圍相同,先加 (5,10] next-key 發現被阻塞,後面 (10,15) 沒有被加上,暫時等待。
  3. Session A,加入 (8,8,8) 和 Session B 的加鎖範圍 (5,10] 衝突,被阻塞。形成 A,B 相互等待的情況。引發死鎖檢測,釋放 Session B.

假如把 insert into t values(8,8,8) 改成 insert into t values(11,11,11) 是可以的,因為 Session B 的間歇鎖 (10,15) 沒有被加上。

分析下死鎖:

mysql> show engine innodb status\G;
------------------------
LATEST DETECTED DEADLOCK
------------------------
2020-03-08 17:04:10 0x7f9be0057700
*** (1) TRANSACTION:
TRANSACTION 836108, ACTIVE 16 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 1653, OS thread handle 140307320846080, query id 1564409 localhost cisco updating
update t set d=d+1 where c=10
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 251 page no 4 n bits 80 index c of table `my_test`.`t` trx id 836108 lock_mode X waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 4; hex 8000000a; asc ;;

*** (2) TRANSACTION:
TRANSACTION 836109, ACTIVE 22 sec inserting
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1
MySQL thread id 1655, OS thread handle 140307455112960, query id 1564410 localhost cisco update
insert into t values(8,8,8)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 251 page no 4 n bits 80 index c of table `my_test`.`t` trx id 836109 lock mode S
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 4; hex 8000000a; asc ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 251 page no 4 n bits 80 index c of table `my_test`.`t` trx id 836109 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 4; hex 8000000a; asc ;;

*** WE ROLL BACK TRANSACTION (1)
  1. (1) TRANSACTION 表明發生死鎖的第一個事務資訊。
  2. (2) TRANSACTION 表明發生死鎖的第二個事務資訊。
  3. WE ROLL BACK TRANSACTION (1) 表明死鎖的處理方案。

針對 (1) TRANSACTION:

  1. (1) WAITING FOR THIS LOCK TO BE GRANTED 表示 update t set d=d+1 where c=10 要申請寫鎖,並處於鎖等待的情況。
  2. 申請的物件是 n_fields 2hex 8000000a;hex 8000000a;, 也就是 id=10 和 c=10 的記錄。

針對 (1) TRANSACTION:

  1. HOLDS THE LOCK(S): 表示當前事務2持有的鎖是 :hex 8000000a;hex 8000000a;.
  2. WAITING FOR THIS LOCK TO BE GRANTED: 表示對於 insert into t values(8,8,8) 進行所等待。
  3. lock_mode X locks gap before rec insert intention waiting: 表明在插入意向鎖時,等待一個間隙鎖( gap before rec)。

所以最後選擇,回滾事務 (1)。

場景10:非唯一索引 order by

Session ASession B
begin;
select * from t where c>=15 and c<=20 order by c desc lock in share mode;
insert into t values(6,6,6);
被阻塞

在分析具體的加鎖過程時,先要分析語句的執行順序。如 Session A 中使用了 ordery by c desc 按照降序排列的語句,這就意味著需要在索引樹 C 上,找到第一個 20 的值,然後向左遍歷。並且由於 C 是非唯一索引 20 的值應該是記錄中最右邊的值。

Session A 的加鎖過程:

  1. 在找到第一個 c=20 的值後,加 next-key (15,20].
  2. 但不會停下,因為無法確定當前 c=20 是最右面的值,繼續遍歷到 c=25,發現不滿足,根據優化2,加 (20,25) 的間隙鎖。
  3. 然後從最左面的 c=20 向左遍歷,找到 c=15,加鎖 next-key (10,15].
  4. 和之前是一樣,無法確定 c=15 是最左面的值,繼續遍歷到 c=10,根據優化2,加(5,10)的間隙鎖 。
  5. 最後由於是 select * 對應主鍵索引 id=10,15,20 加行鎖。

場景 11:INSERT INTO .... SELECT ...

Session ASession B
begin; begin;
insert into t values(1,1,1);
insert into t (id,c,d) select 1,1,1 from t where id=1;
被阻塞

為了保證資料的一致性,對於 INSERT INTO .... SELECT ... 中 select 部分會加 next-key 的讀鎖。

對於 Session A,在插入資料後,有了 id=1 的行鎖。而 Session B 中的 select 雖然是一致性讀,但會加上 id=1 的讀鎖。與 Session A 衝突,所以被阻塞。

場景12:不等號條件裡的等值查詢

begin;
select * from t where id>9 and id<12 order by id desc for update;
  1. 這裡由於是 order by 語句,優化器會先找到第一個比 12 小的值。在索引樹搜尋過程後,其實要找到 id=12 的值,但沒有找到,向右遍歷找到 id=15,所以加鎖 (10,15].

  2. 但由於第一次查詢是等值查詢(在索引樹上搜索),根據優化2,變為間隙鎖 (10,15).

  3. 然後向左遍歷,變為範圍查詢,找到 id=5 這行,加 (0,5] 的 next-key.

場景 13:等值查詢 in

begin;
select id from t where c in(5,20,10) lock in share mode;
mysql> explain select id from t where c in (5,20,10) lock in share mode\G;
*************************** 1. row ***************************
 id: 1
 select_type: SIMPLE
 table: t
 partitions: NULL
 type: range
possible_keys: c
 key: c
 key_len: 5
 ref: NULL
 rows: 3
 filtered: 100.00
 Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)

ERROR:
No query specified

rows=3 並且使用索引 c,說明三個值都是通過 B+ 樹搜尋定位的。

  1. 先查詢 c=5,鎖住 (0,5]. 由於 c 不是唯一索引,向右遍歷到 c=10,開始是等值查詢,加 (5,10).
  2. 查詢 c=15,鎖住 (10,15], 再加 (15,20).
  3. 最後查詢 c=20,鎖住 (15,20]. 再加 (20,25).

可見,在 MySQL 中,鎖是一個個逐步加的。

假設還有一個這樣的語句:

select id from t where c in(5,20,10) order by c desc for update;

由於是 order by c desc,雖然這裡的加鎖範圍沒有變,但是加鎖的順序發生了改變,會按照 c=20,c=10,c=5. 的順序加鎖。雖然說間隙鎖本身並不衝突,但記錄鎖卻會。這樣如果是兩個語句併發的情況,就可能發生死鎖,第一個語句擁有了 c5 的行鎖,請求c=10 的行鎖。當第二個語句,擁有了 c=10 的行鎖,請求 c=5 的行鎖。

場景14:GAP 動態鎖

Session ASession B
begin;
select * from t where id>10 and id<=15 for update;
delete from t where id=10;
成功
insert into t values(10,10,10);
被阻塞。

這裡 insert 被阻塞,就是因為間隙鎖是個動態的概念,Session B 在刪除 id=10 的記錄後,Session A 持有的間隙變大。

對於 Session A 原來持有,(10,15] 和 (15,20] 的 next-key 鎖。 Session B 刪除 id=10 的記錄後,(10,15] 變成了 (5,15] 的間隙。所以之後就插入不回去了。

場景15:update Gap 動態鎖

Session ASession B
begin;
select * from t where id> 5 lock in share mode;
update t set c=1 where c = 5;
成功
update t set c=5 where c = 1;
被阻塞。

Session A 加鎖:(5,10], (10,15], (15,20], (20,25], (25,supermum].

c>5 第一個找到的是 c=10,並且是範圍查詢,沒有優化原則。

Session B 的 update 可以拆成兩步:

  1. 插入 (c=1,id=5).
  2. 刪除 (c=5,id=5).

或者理解成,加(0.5] next-key 和 (5,10) 的間隙鎖,但間隙鎖不衝突。

修改後 Session A 的鎖變為;

(c=1, 10], (10,15], (15,20], (20,25], (25,supermum].

接下來:update t set c=1 where c = 1

  1. 插入 (c=5,id=5).
  2. 刪除 (c=1,id=5).

第一步插入意向鎖和間隙鎖衝突。

Top

參考

InnoDB-locking

加鎖過程

explain rows