關於 InnoDB 鎖的超全總結

幾個月以前,開始深刻學習 MySQL 。提及數據庫,併發控制是其中很重要的一部分。因而,就這樣開起了 MySQL 鎖的學習,隨着學習的深刻,發現想要更好的理解鎖,須要瞭解 MySQL 事務,數據底層的存儲方式,MySQL 的執行流程,特別是索引的選擇等。html

在學習期間,查找了很多資料,現根據我的的理解總結下來,方便往後複習。mysql

InnoDB 鎖一覽

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

Shared and Exclusive Locks

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

與行級別的共享鎖和排他鎖相似的,還有表級別的共享鎖和排他鎖。如 LOCK TABLES ... WRITE/READ 等命令,實現的就是表級鎖。session

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 衝突的問題,意向鎖就更不被提起來了。spa

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.

加鎖規則

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

  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. 等值查詢和遍歷有什麼關係?

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

應用場景

表結構以下:

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 A Session B Session 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 A Session B Session 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 A Session 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,用於保證惟一性,畫個圖以下。

image-20200304140317000

由圖中可知,因爲非惟一索引存在主鍵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 A Session B Session 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 A Session B Session 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 A Session B Session 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 A Session B Session 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 A Session 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 A Session 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 A Session 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 A Session 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 A Session 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 A Session 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).

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

參考

InnoDB-locking

加鎖過程

explain rows

相關文章
相關標籤/搜索