幾個月以前,開始深刻學習 MySQL 。提及數據庫,併發控制是其中很重要的一部分。因而,就這樣開起了 MySQL 鎖的學習,隨着學習的深刻,發現想要更好的理解鎖,須要瞭解 MySQL 事務,數據底層的存儲方式,MySQL 的執行流程,特別是索引的選擇等。html
在學習期間,查找了很多資料,現根據我的的理解總結下來,方便往後複習。mysql
先從 MySQL 官網的鎖介紹開始,來逐一認識下這些讓咱們夜不能寐的小王八蛋:sql
這二位正式稱呼呢,就是共享鎖和排他鎖,其實就是咱們常說的讀鎖和寫鎖。它們之間的互斥規則,想必都清楚,就不贅述了。但有一點須要注意,共享鎖和排他鎖是標準的實現行級別的鎖。舉例來講,當給 select 語句應用 lock in share mode
或者 for update
,或者更新某條記錄時,加的都是行級別的鎖。數據庫
與行級別的共享鎖和排他鎖相似的,還有表級別的共享鎖和排他鎖。如 LOCK TABLES ... WRITE/READ
等命令,實現的就是表級鎖。session
在 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 lock ,就是常說的行鎖。InnoDB 中,表都以索引的形式存在,每個索引對應一顆 B+ 樹,這裏的行鎖鎖的就是 B+ 中的索引記錄。以前提到的共享鎖和排他鎖,就是將鎖加在這裏。
Gap Locks, 間隙鎖鎖住的是索引記錄間的空隙,是爲了解決幻讀問題被引入的。有一點須要注意,間隙鎖和間隙鎖自己之間並不衝突,僅僅和插入這個操做發生衝突。
next-key lock 是行鎖(Record)和間隙鎖的並集。在 RR 級別下,InnoDB 使用 next-key 鎖進行樹搜索和索引掃描。記住這句話,加鎖的基本單位是 next-key lock.
該加鎖原則由林曉斌老師刷代碼後總結,符合的版本以下:
規則包括:兩個「原則」、兩個「優化」和一個「bug」。
解釋下容易理解錯誤的地方:
對優化 2 的說明:
從等值查詢的值開始,向右遍歷到第一個不知足等值條件記錄結束,而後將不知足條件記錄的 next-key 退化爲間隙鎖。
等值查詢和遍歷有什麼關係?
在分析加鎖行爲時,必定要從索引的數據結構開始。經過樹搜索的方式定位索引記錄時,用的是"等值查詢",而遍歷對應的是在記錄上向前或向後掃描的過程。
表結構以下:
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);
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 來講:
對於 Session B 來講:
對於 Session C 來講:
這裏能夠看出,行鎖和間隙鎖都是有 next-key 鎖知足必定後條件後轉換的,加鎖的默認單位是 next-key.
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:
Session B:
Session C:
能夠看出,加鎖實際上是在索引上,而且只加在訪問到的記錄上,若是想要在 lock in share mode 下避免數據被更新,須要引入覆蓋索引不能包含的字段。
假設將 Session A 的語句改爲 select id from t where c=5 for update;
, for update 表示可能當前事務要更新數據,因此也會給知足的條件的主鍵索引加鎖。這時 Session B 就會被阻塞了。
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,用於保證惟一性,畫個圖以下。
由圖中可知,因爲非惟一索引存在主鍵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 的狀況。
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:
Session B:
Session C:
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:
Session B:
Session C:
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:
這個 bug 在 8.0.18 後已經修復了
對於 Session B 和 Session C 均和加鎖的範圍衝突。
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:
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:
因此對於 session B 來講,就不在阻塞。
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 |
假如把 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) TRANSACTION
代表發生死鎖的第一個事務信息。(2) TRANSACTION
代表發生死鎖的第二個事務信息。WE ROLL BACK TRANSACTION (1)
代表死鎖的處理方案。針對 (1) TRANSACTION
:
(1) WAITING FOR THIS LOCK TO BE GRANTED
表示 update t set d=d+1 where c=10
要申請寫鎖,並處於鎖等待的狀況。n_fields 2
,hex 8000000a;
和 hex 8000000a;
, 也就是 id=10 和 c=10 的記錄。針對 (1) TRANSACTION
:
HOLDS THE LOCK(S):
表示當前事務2持有的鎖是 :hex 8000000a;
和 hex 8000000a;
.WAITING FOR THIS LOCK TO BE GRANTED:
表示對於 insert into t values(8,8,8)
進行所等待。lock_mode X locks gap before rec insert intention waiting
: 代表在插入意向鎖時,等待一個間隙鎖( gap before rec
)。因此最後選擇,回滾事務 (1)。
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 的加鎖過程:
select *
對應主鍵索引 id=10,15,20 加行鎖。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 衝突,因此被阻塞。
begin; select * from t where id>9 and id<12 order by id desc for update;
這裏因爲是 order by
語句,優化器會先找到第一個比 12 小的值。在索引樹搜索過程後,其實要找到 id=12 的值,但沒有找到,向右遍歷找到 id=15,因此加鎖 (10,15].
但因爲第一次查找是等值查找(在索引樹上搜索),根據優化2,變爲間隙鎖 (10,15).
而後向左遍歷,變爲範圍查詢,找到 id=5 這行,加 (0,5] 的 next-key.
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+ 樹搜索定位的。
可見,在 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 的行鎖。
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] 的間隙。因此以後就插入不回去了。
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 能夠拆成兩步:
或者理解成,加(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
第一步插入意向鎖和間隙鎖衝突。