在第20和21篇文章中,我和你介紹了 InnoDB 的間隙鎖、next-key lock,以及加鎖規則。在這兩篇文章的評論區,出現了不少高質量的留言。我以爲經過分析這些問題,能夠
幫助你加深對加鎖規則的理解。bash
因此,我就從中挑選了幾個有表明性的問題,構成了今天這篇答疑文章的主題,即:用動態的觀點看加鎖。session
原則 1:加鎖的基本單位是 next-key lock。但願你還記得,next-key lock 是前開後閉區間。
原則 2:查找過程當中訪問到的對象纔會加鎖。數據結構
優化 1:索引上的等值查詢,給惟一索引加鎖的時候,next-key lock 退化爲行鎖。
優化 2:索引上的等值查詢,向右遍歷時且最後一個值不知足等值條件的時候,next-key lock 退化爲間隙鎖。併發
一個 bug:惟一索引上的範圍查詢會訪問到不知足條件的第一個值爲止。優化
接下來,咱們的討論仍是基於下面這個表 t:spa
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);
有同窗對「等值查詢」提出了疑問:等值查詢和「遍歷」有什麼區別?爲何咱們文章的例子裏面,where 條件是不等號,這個過程裏也有等值查詢?線程
咱們一塊兒來看下這個例子,分析一下這條查詢語句的加鎖範圍:對象
begin; select * from t where id>9 and id<12 order by id desc for update;
利用上面的加鎖規則,咱們知道這個語句的加鎖範圍是主鍵索引上的 (0,5]、(5,10] 和 (10,15)。也就是說,id=15 這一行,並無被加上行鎖。爲何呢?blog
咱們說加鎖單位是 next-key lock,都是前開後閉區間,可是這裏用到了優化 2,即索引上的等值查詢,向右遍歷的時候 id=15 不知足條件,因此 next-key lock 退化爲了間隙鎖(10, 15)。索引
可是,咱們的查詢語句中 where 條件是大於號和小於號,這裏的「等值查詢」又是從哪裏來的呢?
要知道,加鎖動做是發生在語句執行過程當中的,因此你在分析加鎖行爲的時候,要從索引上的數據結構開始。這裏,我再把這個過程拆解一下。
如圖 1 所示,是這個表的索引 id 的示意圖。
圖 1 索引 id 示意圖
1. 首先這個查詢語句的語義是 order by id desc,要拿到知足條件的全部行,優化器必須先找到「第一個 id<12 的值」。
2. 這個過程是經過索引樹的搜索過程獲得的,在引擎內部,實際上是要找到 id=12 的這個值,只是最終沒找到,但找到了 (10,15) 這個間隙。
3. 而後向左遍歷,在遍歷過程當中,就不是等值查詢了,會掃描到 id=5 這一行,因此會加一個 next-key lock (0,5]。
也就是說,在執行過程當中,經過樹搜索的方式定位記錄的時候,用的是「等值查詢」的方法。
與上面這個例子對應的,是 @發條橙子同窗提出的問題:下面這個語句的加鎖範圍是什麼?
begin; select id from t where c in(5,20,10) lock in share mode;
這條查詢語句裏用的是 in,咱們先來看這條語句的 explain 結果。
圖 2 in 語句的 explain 結果
能夠看到,這條 in 語句使用了索引 c 而且 rows=3,說明這三個值都是經過 B+ 樹搜索定位的。
在查找 c=5 的時候,先鎖住了 (0,5]。可是由於 c 不是惟一索引,爲了確認還有沒有別的記錄 c=5,就要向右遍歷,找到 c=10 才確認沒有了,這個過程知足優化 2,因此加了間
隙鎖 (5,10)。
一樣的,執行 c=10 這個邏輯的時候,加鎖的範圍是 (5,10] 和 (10,15);執行 c=20 這個邏輯的時候,加鎖的範圍是 (15,20] 和 (20,25)。
經過這個分析,咱們能夠知道,這條語句在索引 c 上加的三個記錄鎖的順序是:先加 c=5的記錄鎖,再加 c=10 的記錄鎖,最後加 c=20 的記錄鎖。
你可能會說,這個加鎖範圍,不就是從 (5,25) 中去掉 c=15 的行鎖嗎?爲何這麼麻煩地分段說呢?
由於我要跟你強調這個過程:這些鎖是「在執行過程當中一個一個加的」,而不是一次性加上去的。
理解了這個加鎖過程以後,咱們就能夠來分析下面例子中的死鎖問題了。
若是同時有另一個語句,是這麼寫的:
select id from t where c in(5,20,10) order by c desc for update;
此時的加鎖範圍,又是什麼呢?
咱們如今都知道間隙鎖是不互鎖的,可是這兩條語句都會在索引 c 上的 c=五、十、20 這三行記錄上加記錄鎖。
這裏你須要注意一下,因爲語句裏面是 order by c desc, 這三個記錄鎖的加鎖順序,是先鎖 c=20,而後 c=10,最後是 c=5。
也就是說,這兩條語句要加鎖相同的資源,可是加鎖順序相反。當這兩條語句併發執行的時候,就可能出現死鎖。
關於死鎖的信息,MySQL 只保留了最後一個死鎖的現場,但這個現場仍是不完備的。
有同窗在評論區留言到,但願我能展開一下怎麼看死鎖。如今,我就來簡單分析一下上面這個例子的死鎖現場。
圖 3 是在出現死鎖後,執行 show engine innodb status 命令獲得的部分輸出。這個命令
會輸出不少信息,有一節 LATESTDETECTED DEADLOCK,就是記錄的最後一次死鎖信
息。
圖 3 死鎖現場
咱們來看看這圖中的幾個關鍵信息。
一、 TRANSACTION,是第一個事務的信息;
二、 TRANSACTION,是第二個事務的信息;
三、WE ROLL BACK TRANSACTION (1),是最終的處理結果,表示回滾了第一個事務。
從上面這些信息中,咱們就知道:
1. 「lock in share mode」的這條語句,持有 c=5 的記錄鎖,在等 c=10 的鎖;
2. 「for update」這個語句,持有 c=20 和 c=10 的記錄鎖,在等 c=5 的記錄鎖。
所以致使了死鎖。這裏,咱們能夠獲得兩個結論:
1. 因爲鎖是一個個加的,要避免死鎖,對同一組資源,要按照儘可能相同的順序訪問;
2. 在發生死鎖的時刻,for update 這條語句佔有的資源更多,回滾成本更大,因此InnoDB 選擇了回滾成本更小的 lock in share mode 語句,來回滾。
看完死鎖,咱們再來看一個鎖等待的例子。
在第 21 篇文章的評論區,@Geek_9ca34e 同窗作了一個有趣驗證,我把復現步驟列出來:
圖 4 delete 致使間隙變化
能夠看到,因爲 session A 並無鎖住 c=10 這個記錄,因此 session B 刪除 id=10 這一行是能夠的。可是以後,session B 再想 insert id=10 這一行回去就不行了。
如今咱們一塊兒看一下此時 show engine innodb status 的結果,看看能不能給咱們一些提示。鎖信息是在這個命令輸出結果的 TRANSACTIONS 這一節。你能夠在文稿中看到這張圖片
圖 5 鎖等待信息
咱們來看幾個關鍵信息。
1. index PRIMARY of table `test`.`t` ,表示這個語句被鎖住是由於表 t 主鍵上的某個鎖。
2. lock_mode X locks gap before rec insert intention waiting 這裏有幾個信息:
3. 那麼這個 gap 是在哪一個記錄以前的呢?接下來的 0~4 這 5 行的內容就是這個記錄的信息。
4. n_fields 5 也表示了,這一個記錄有 5 列:
所以,咱們就知道了,因爲 delete 操做把 id=10 這一行刪掉了,原來的兩個間隙(5,10)、(10,15)變成了一個 (5,15)。
說到這裏,你能夠聯合起來再思考一下這兩個現象之間的關聯:
1. session A 執行完 select 語句後,什麼都沒作,但它加鎖的範圍忽然「變大」了;
2. 第 21 篇文章的課後思考題,當咱們執行 select * from t where c>=15 and c<=20order by c desc lock in share mode; 向左掃描到 c=10 的時候,要把 (5, 10] 鎖起來
也就是說,所謂「間隙」,其實根本就是由「這個間隙右邊的那個記錄」定義的。
看過了 insert 和 delete 的加鎖例子,咱們再來看一個 update 語句的案例。在留言區中@信信 同窗作了這個試驗:
圖 6 update 的例子
你能夠本身分析一下,session A 的加鎖範圍是索引 c 上的 (5,10]、(10,15]、(15,20]、(20,25] 和 (25,supremum]。以後 session B 的第一個 update 語句,要把 c=5 改爲 c=1,你能夠理解爲兩步:
1. 插入 (c=1, id=5) 這個記錄;
2. 刪除 (c=5, id=5) 這個記錄。
按照咱們上一節說的,索引 c 上 (5,10) 間隙是由這個間隙右邊的記錄,也就是 c=10 定義的。因此經過這個操做,session A 的加鎖範圍變成了圖 7 所示的樣子:
注意:根據 c>5 查到的第一個記錄是 c=10,所以不會加 (0,5] 這個 next-key lock。
圖 7 session B 修改後, session A 的加鎖範圍
好,接下來 session B 要執行 update t set c = 5 where c = 1 這個語句了,同樣地能夠拆成兩步:
1. 插入 (c=5, id=5) 這個記錄;
2. 刪除 (c=1, id=5) 這個記錄。
第一步試圖在已經加了間隙鎖的 (1,10) 中插入數據,因此就被堵住了。
今天這篇文章,我用前面第 20和第 21 篇文章評論區的幾個問題,再次跟你複習了加鎖規則。而且,我和你重點說明了,分析加鎖範圍時,必定要配合語句執行邏輯來進行。
在我看來,每一個想認真瞭解 MySQL 原理的同窗,應該都要可以作到:經過 explain 的結果,就可以腦補出一個 SQL 語句的執行流程。達到這樣的程度,纔算是對索引組織表、索
引、鎖的概念有了比較清晰的認識。你一樣也能夠用這個方法,來驗證本身對這些知識點的掌握程度。
在分析這些加鎖規則的過程當中,我也順便跟你介紹了怎麼看 show engine innodb status輸出結果中的事務信息和死鎖信息,但願這些內容對你之後分析現場能有所幫助。
老規矩,即使是答疑文章,我也仍是要留一個課後問題給你的。
上面咱們提到一個很重要的點:所謂「間隙」,其實根本就是由「這個間隙右邊的那個記錄」定義的。
那麼,一個空表有間隙嗎?這個間隙是由誰定義的?你怎麼驗證這個結論呢?
你能夠把你關於分析和驗證方法寫在留言區,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
我在上一篇文章最後留給的問題,是分享一下你關於業務監控的處理經驗。
在這篇文章的評論區,不少同窗都分享了不錯的經驗。這裏,我就選擇幾個比較典型的留言,和你分享吧:
@老楊同志 回答得很詳細。他的主要思路就是關於服務狀態和服務質量的監控。其中,服務狀態的監控,通常均可以用外部系統來實現;而服務的質量的監控,就要經過接口
的響應時間來統計。
@Ryoma 同窗,提到服務中使用了 healthCheck 來檢測,其實跟咱們文中提到的select 1 的模式相似。
@強哥 同窗,按照監控的對象,將監控分紅了基礎監控、服務監控和業務監控,並分享了每種監控須要關注的對象。
這些都是很好的經驗,你也能夠根據具體的業務場景借鑑適合本身的方案。