在上一篇文章中,我和你介紹了間隙鎖和 next-key lock 的概念,可是並無說明加鎖規則。間隙鎖的概念理解起來確實有點兒難,尤爲在配合上行鎖之後,很容易在判斷是否會
出現鎖等待的問題上犯錯。mysql
因此今天,咱們就先從這個加鎖規則開始吧。sql
首先說明一下,這些加鎖規則我沒在別的地方看到過有相似的總結,之前我本身判斷的時候都是想着代碼裏面的實現來腦補的。此次爲了總結成不看代碼的同窗也能理解的規則,
是我又從新刷了代碼臨時總結出來的。因此,這個規則有如下兩條前提說明:數據庫
1. MySQL 後面的版本可能會改變加鎖策略,因此這個規則只限於截止到如今的最新版本,即 5.x 系列 <=5.7.24,8.0 系列 <=8.0.13。
2. 若是你們在驗證中有發現 bad case 的話,請提出來,我會再補充進這篇文章,使得一塊兒學習本專欄的全部同窗都能受益。安全
由於間隙鎖在可重複讀隔離級別下才有效,因此本篇文章接下來的描述,若沒有特殊說明,默認是可重複讀隔離級別。bash
我總結的加鎖規則裏面,包含了兩個「原則」、兩個「優化」和一個「bug」。session
1. 原則 1:加鎖的基本單位是 next-key lock。但願你還記得,next-key lock 是前開後閉區間。
2. 原則 2:查找過程當中訪問到的對象纔會加鎖。
3. 優化 1:索引上的等值查詢,給惟一索引加鎖的時候,next-key lock 退化爲行鎖。
4. 優化 2:索引上的等值查詢,向右遍歷時且最後一個值不知足等值條件的時候,next-key lock 退化爲間隙鎖。
5. 一個 bug:惟一索引上的範圍查詢會訪問到不知足條件的第一個值爲止。學習
我仍是以上篇文章的表 t 爲例,和你解釋一下這些規則。表 t 的建表語句和初始化語句以下測試
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);
接下來的例子基本都是配合着圖片說明的,因此我建議你能夠對照着文稿看,有些例子可能會「毀三觀」,也建議你讀完文章後親手實踐一下。優化
第一個例子是關於等值條件操做間隙:spa
圖 1 等值查詢的間隙鎖
因爲表 t 中沒有 id=7 的記錄,因此用咱們上面提到的加鎖規則判斷一下的話:
1. 根據原則 1,加鎖單位是 next-key lock,session A 加鎖範圍就是 (5,10];
2. 同時根據優化 2,這是一個等值查詢 (id=7),而 id=10 不知足查詢條件,next-keylock 退化成間隙鎖,所以最終加鎖的範圍是 (5,10)。
因此,session B 要往這個間隙裏面插入 id=8 的記錄會被鎖住,可是 session C 修改id=10 這行是能夠的。
session A
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> update t set d=d+1 where id=7; Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0
session B
session C
mysql> update t set d=d+1 where id=10; Query OK, 1 row affected Rows matched: 1 Changed: 1 Warnings: 0
第二個例子是關於覆蓋索引上的鎖:
圖 2 只加在非惟一索引上的鎖
看到這個例子,你是否是有一種「該鎖的不鎖,不應鎖的亂鎖」的感受?咱們來分析一下吧。
這裏 session A 要給索引 c 上 c=5 的這一行加上讀鎖。
1. 根據原則 1,加鎖單位是 next-key lock,所以會給 (0,5] 加上 next-key lock。
2. 要注意 c 是普通索引,所以僅訪問 c=5 這一條記錄是不能立刻停下來的,須要向右遍歷,查到 c=10 才放棄。根據原則 2,訪問到的都要加鎖,所以要給 (5,10] 加 next-key lock。
3. 可是同時這個符合優化 2:等值判斷,向右遍歷,最後一個值不知足 c=5 這個等值條件,所以退化成間隙鎖 (5,10)。
4. 根據原則 2 ,只有訪問到的對象纔會加鎖,這個查詢使用覆蓋索引,並不須要訪問主鍵索引,因此主鍵索引上沒有加任何鎖,這就是爲何 session B 的 update 語句能夠執行完成。
但 session C 要插入一個 (7,7,7) 的記錄,就會被 session A 的間隙鎖 (5,10) 鎖住。須要注意,在這個例子中,lock in share mode 只鎖覆蓋索引,可是若是是 for update
就不同了。 執行 for update 時,系統會認爲你接下來要更新數據,所以會順便給主鍵索引上知足條件的行加上行鎖。
這個例子說明,鎖是加在索引上的;同時,它給咱們的指導是,若是你要用 lock in sharemode 來給行加讀鎖避免數據被更新的話,就必須得繞過覆蓋索引的優化,在查詢字段中
加入索引中不存在的字段。好比,將 session A 的查詢語句改爲 select d from t wherec=5 lock in share mode。你能夠本身驗證一下效果。
session A
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select id from t where c=5 lock in share mode; +----+ | id | +----+ | 5 | +----+ 1 row in set (0.01 sec)
session B
mysql> update t set d=d+1 where id=5; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
session C
第三個例子是關於範圍查詢的。
舉例以前,你能夠先思考一下這個問題:對於咱們這個表 t,下面這兩條查詢語句,加鎖範圍相同嗎?
mysql> select * from t where id=10 for update; mysql> select * from t where id>=10 and id<11 for update;
你可能會想,id 定義爲 int 類型,這兩個語句就是等價的吧?其實,它們並不徹底等價。
在邏輯上,這兩條查語句確定是等價的,可是它們的加鎖規則不太同樣。如今,咱們就讓session A 執行第二個查詢語句,來看看加鎖效果。
圖 3 主鍵索引上範圍查詢的鎖
如今咱們就用前面提到的加鎖規則,來分析一下 session A 會加什麼鎖呢?
1. 開始執行的時候,要找到第一個 id=10 的行,所以本該是 next-key lock(5,10]。 根據優化 1, 主鍵 id 上的等值條件,退化成行鎖,只加了 id=10 這一行的行鎖。
2. 範圍查找就日後繼續找,找到 id=15 這一行停下來,所以須要加 next-keylock(10,15]。
因此,session A 這時候鎖的範圍就是主鍵索引上,行鎖 id=10 和 next-keylock(10,15]。這樣,session B 和 session C 的結果你就能理解了。
這裏你須要注意一點,首次 session A 定位查找 id=10 的行的時候,是當作等值查詢來判斷的,而向右掃描到 id=15 的時候,用的是範圍查詢判斷。
session A
mysql> begin; Query OK, 0 rows affected mysql> select * from t where id>=10 and id<11 for update; +----+----+----+ | id | c | d | +----+----+----+ | 10 | 10 | 12 | +----+----+----+ 1 row in set
session B
session C
接下來,咱們再看兩個範圍查詢加鎖的例子,你能夠對照着案例三來看。
須要注意的是,與案例三不一樣的是,案例四中查詢語句的 where 部分用的是字段 c。
圖 4 非惟一索引範圍鎖
此次 session A 用字段 c 來判斷,加鎖規則跟案例三惟一的不一樣是:在第一次用 c=10 定位記錄的時候,索引 c 上加了 (5,10] 這個 next-key lock 後,因爲索引 c 是非惟一索引,
沒有優化規則,也就是說不會蛻變爲行鎖,所以最終 sesion A 加的鎖是,索引 c 上的(5,10] 和 (10,15] 這兩個 next-key lock。
因此從結果上來看,sesson B 要插入(8,8,8) 的這個 insert 語句時就被堵住了。
這裏須要掃描到 c=15 才中止掃描,是合理的,由於 InnoDB 要掃到 c=15,才知道不須要繼續日後找了。
session A
mysql> begin; Query OK, 0 rows affected mysql> select * from t where c>=10 and c<11 for update; +----+----+----+ | id | c | d | +----+----+----+ | 10 | 10 | 12 | +----+----+----+ 1 row in set
session B
mysql> insert into t values(8,8,8); ERROR 1062 (23000): Duplicate entry '8' for key 'PRIMARY'
session C
前面的四個案例,咱們已經用到了加鎖規則中的兩個原則和兩個優化,接下來再看一個關於加鎖規則中 bug 的案例。
圖 5 惟一索引範圍鎖的 bug
session A 是一個範圍查詢,按照原則 1 的話,應該是索引 id 上只加 (10,15] 這個 next-key lock,而且由於 id 是惟一鍵,因此循環判斷到 id=15 這一行就應該中止了。
可是實現上,InnoDB 會往前掃描到第一個不知足條件的行爲止,也就是 id=20。並且因爲這是個範圍掃描,所以索引 id 上的 (15,20] 這個 next-key lock 也會被鎖上。
因此你看到了,session B 要更新 id=20 這一行,是會被鎖住的。一樣地,session C 要插入 id=16 的一行,也會被鎖住。
照理說,這裏鎖住 id=20 這一行的行爲,實際上是沒有必要的。由於掃描到 id=15,就能夠肯定不用日後再找了。但實現上仍是這麼作了,所以我認爲這是個 bug。
我也曾找社區的專家討論過,官方 bug 系統上也有提到,可是並未被 verified。因此,認爲這是 bug 這個事兒,也只能算個人一家之言,若是你有其餘看法的話,也歡迎你提出來。
session A
mysql> begin; Query OK, 0 rows affected mysql> select * from t where id>10 and id<=15 for update; +----+----+----+ | id | c | d | +----+----+----+ | 15 | 15 | 16 | +----+----+----+ 1 row in set
session B
session C
接下來的例子,是爲了更好地說明「間隙」這個概念。這裏,我給表 t 插入一條新記錄。
mysql> insert into t values(30,10,30);
新插入的這一行 c=10,也就是說如今表裏有兩個 c=10 的行。那麼,這時候索引 c 上的間隙是什麼狀態了呢?你要知道,因爲非惟一索引上包含主鍵的值,因此是不可能存在「相同」的兩行的。
圖 6 非惟一索引等值的例子
能夠看到,雖然有兩個 c=10,可是它們的主鍵值 id 是不一樣的(分別是 10 和 30),所以這兩個 c=10 的記錄之間,也是有間隙的。
圖中我畫出了索引 c 上的主鍵 id。爲了跟間隙鎖的開區間形式進行區別,我用(c=10,id=30) 這樣的形式,來表示索引上的一行。
如今,咱們來看一下案例六。
此次咱們用 delete 語句來驗證。注意,delete 語句加鎖的邏輯,其實跟 select ... forupdate 是相似的,也就是我在文章開始總結的兩個「原則」、兩個「優化」和一個「bug」。
圖 7 delete 示例
這時,session A 在遍歷的時候,先訪問第一個 c=10 的記錄。一樣地,根據原則 1,這裏加的是 (c=5,id=5) 到 (c=10,id=10) 這個 next-key lock。
而後,session A 向右查找,直到碰到 (c=15,id=15) 這一行,循環才結束。根據優化 2,這是一個等值查詢,向右查找到了不知足條件的行,因此會退化成 (c=10,id=10) 到
(c=15,id=15) 的間隙鎖。
也就是說,這個 delete 語句在索引 c 上的加鎖範圍,就是下圖中藍色區域覆蓋的部分。
圖 8 delete 加鎖效果示例
這個藍色區域左右兩邊都是虛線,表示開區間,即 (c=5,id=5) 和 (c=15,id=15) 這兩行上都沒有鎖。
session A
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> delete from t where c=10; Query OK, 1 row affected (0.00 sec)
session B
session C
mysql> update t set d=d+1 where c=15; Query OK, 1 row affected Rows matched: 1 Changed: 1 Warnings: 0
例子 6 也有一個對照案例,場景以下所示:
圖 9 limit 語句加鎖
這個例子裏,session A 的 delete 語句加了 limit 2。你知道表 t 裏 c=10 的記錄其實只有兩條,所以加不加 limit 2,刪除的效果都是同樣的,可是加鎖的效果卻不一樣。能夠看
到,session B 的 insert 語句執行經過了,跟案例六的結果不一樣。
這是由於,案例七裏的 delete 語句明確加了 limit 2 的限制,所以在遍歷到 (c=10,id=30) 這一行以後,知足條件的語句已經有兩條,循環就結束了。
所以,索引 c 上的加鎖範圍就變成了從(c=5,id=5) 到(c=10,id=30) 這個前開後閉區間,以下圖所示:
圖 10 帶 limit 2 的加鎖效果
能夠看到,(c=10,id=30)以後的這個間隙並無在加鎖範圍裏,所以 insert 語句插入c=12 是能夠執行成功的。
這個例子對咱們實踐的指導意義就是,在刪除數據的時候儘可能加 limit。這樣不只能夠控制刪除數據的條數,讓操做更安全,還能夠減少加鎖的範圍
session A
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> delete from t where c=10 limit 2; Query OK, 1 row affected (0.01 sec)
session B
實際測試和老師不符
前面的例子中,咱們在分析的時候,是按照 next-key lock 的邏輯來分析的,由於這樣分析比較方便。最後咱們再看一個案例,目的是說明:next-key lock 其實是間隙鎖和行
鎖加起來的結果。
你必定會疑惑,這個概念不是一開始就說了嗎?不要着急,咱們先來看下面這個例子:
圖 11 案例八的操做序列
如今,咱們按時間順序來分析一下爲何是這樣的結果。
1. session A 啓動事務後執行查詢語句加 lock in share mode,在索引 c 上加了 next-key lock(5,10] 和間隙鎖 (10,15);
2. session B 的 update 語句也要在索引 c 上加 next-key lock(5,10] ,進入鎖等待;
3. 而後 session A 要再插入 (8,8,8) 這一行,被 session B 的間隙鎖鎖住。因爲出現了死鎖,InnoDB 讓 session B 回滾。
你可能會問,session B 的 next-key lock 不是還沒申請成功嗎?
實際上是這樣的,session B 的「加 next-key lock(5,10] 」操做,實際上分紅了兩步,先是加 (5,10) 的間隙鎖,加鎖成功;而後加 c=10 的行鎖,這時候才被鎖住的。
也就是說,咱們在分析加鎖規則的時候能夠用 next-key lock 來分析。可是要知道,具體執行的時候,是要分紅間隙鎖和行鎖兩段來執行的。
session A
mysql> begin; Query OK, 0 rows affected (0.01 sec) mysql> select id from t where c=10 lock in share mode; +----+ | id | +----+ | 10 | +----+ 1 row in set (0.00 sec) mysql> insert into t values(8,8,8); ERROR 1062 (23000): Duplicate entry '8' for key 'PRIMARY'
session B
mysql> update t set d=d+1 where c=10; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
這裏我再次說明一下,咱們上面的全部案例都是在可重複讀隔離級別 (repeatable-read)下驗證的。同時,可重複讀隔離級別遵照兩階段鎖協議,全部加鎖的資源,都是在事務提
交或者回滾的時候才釋放的。
在最後的案例中,你能夠清楚地知道 next-key lock 其實是由間隙鎖加行鎖實現的。若是切換到讀提交隔離級別 (read-committed) 的話,就好理解了,過程當中去掉間隙鎖的部
分,也就是隻剩下行鎖的部分。
其實讀提交隔離級別在外鍵場景下仍是有間隙鎖,相對比較複雜,咱們今天先不展開。
另外,在讀提交隔離級別下還有一個優化,即:語句執行過程當中加上的行鎖,在語句執行完成後,就要把「不知足條件的行」上的行鎖直接釋放了,不須要等到事務提交。
也就是說,讀提交隔離級別下,鎖的範圍更小,鎖的時間更短,這也是很多業務都默認使用讀提交隔離級別的緣由。
不過,我但願你學過今天的課程之後,能夠對 next-key lock 的概念有更清晰的認識,而且會用加鎖規則去判斷語句的加鎖範圍。
在業務須要使用可重複讀隔離級別的時候,可以更細緻地設計操做數據庫的語句,解決幻讀問題的同時,最大限度地提高系統並行處理事務的能力。
通過這篇文章的介紹,你再看一下上一篇文章最後的思考題,再來嘗試分析一次。我把題目從新描述和簡化一下:仍是咱們在文章開頭初始化的表 t,裏面有 6 條記錄,圖
12 的語句序列中,爲何 session B 的 insert 操做,會被鎖住呢?
對於那些你本身沒法解釋的結果,能夠發到評論區裏,後面我爭取挑一些有趣的案例在文章中分析。
你能夠把你關於思考題的分析寫在留言區,也能夠分享你本身設計的鎖驗證方案,我會在下一篇文章的末尾選取有趣的評論跟你們分享。感謝你的收聽,也歡迎你把這篇文章分享
給更多的朋友一塊兒閱讀。
圖 12 鎖分析思考題
另外,若是你有興趣多作一些實驗的話,能夠設計好語句序列,在執行以前先本身分析一下,而後實際地驗證結果是否跟你的分析一致。
對於那些你本身沒法解釋的結果,能夠發到評論區裏,後面我爭取挑一些有趣的案例在文章中分析。
你能夠把你關於思考題的分析寫在留言區,也能夠分享你本身設計的鎖驗證方案,我會在下一篇文章的末尾選取有趣的評論跟你們分享。感謝你的收聽,也歡迎你把這篇文章分享
給更多的朋友一塊兒閱讀。
上期的問題,我在本期繼續做爲了課後思考題,因此會在下篇文章再一塊兒公佈「答案」。這裏,我展開回答一下評論區幾位同窗的問題。
@令狐少俠 說,之前一直認爲間隙鎖只在二級索引上有。如今你知道了,有間隙的地方就可能有間隙鎖。
@浪裏白條 同窗問,若是是 varchar 類型,加鎖規則是什麼樣的。回答:實際上在判斷間隙的時候,varchar 和 int 是同樣的,排好序之後,相鄰兩個值之
間就有間隙。
有幾位同窗提到說,上一篇文章本身驗證的結果跟案例一不一樣,就是在 session A 執行完這兩個語句:
begin; select * from t where d=5 for update; /*Q1*/
之後,session B 的 update 和 session C 的 insert 都會被堵住。這是否是跟文章的結論矛盾?
其實不是的,這個例子用的是反證假設,就是假設不堵住,會出現問題;而後,推導出session A 須要鎖整個表全部的行和全部間隙。