MySQL實戰45講學習筆記:第三十講

1、複習一下加鎖規則

在第20和21篇文章中,我和你介紹了 InnoDB 的間隙鎖、next-key lock,以及加鎖規則。在這兩篇文章的評論區,出現了不少高質量的留言。我以爲經過分析這些問題,能夠
幫助你加深對加鎖規則的理解。bash

因此,我就從中挑選了幾個有表明性的問題,構成了今天這篇答疑文章的主題,即:用動態的觀點看加鎖。session

爲了方便你理解,咱們再一塊兒複習一下加鎖規則。這個規則中,包含了兩個「原則」、兩個「優化」和一個「bug」:

原則 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);

2、不等號條件裏的等值查詢

有同窗對「等值查詢」提出了疑問:等值查詢和「遍歷」有什麼區別?爲何咱們文章的例子裏面,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]。

也就是說,在執行過程當中,經過樹搜索的方式定位記錄的時候,用的是「等值查詢」的方法。

3、等值查詢的過程

與上面這個例子對應的,是 @發條橙子同窗提出的問題:下面這個語句的加鎖範圍是什麼?

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 只保留了最後一個死鎖的現場,但這個現場仍是不完備的。

有同窗在評論區留言到,但願我能展開一下怎麼看死鎖。如今,我就來簡單分析一下上面這個例子的死鎖現場。

4、怎麼看死鎖?

圖 3 是在出現死鎖後,執行 show engine innodb status 命令獲得的部分輸出。這個命令
會輸出不少信息,有一節 LATESTDETECTED DEADLOCK,就是記錄的最後一次死鎖信
息。

圖 3 死鎖現場

咱們來看看這圖中的幾個關鍵信息。

1. 這個結果分紅三部分:

一、 TRANSACTION,是第一個事務的信息;

二、 TRANSACTION,是第二個事務的信息;

三、WE ROLL BACK TRANSACTION (1),是最終的處理結果,表示回滾了第一個事務。

2. 第一個事務的信息中:

  • WAITING FOR THIS LOCK TO BE GRANTED,表示的是這個事務在等待的鎖信息;
  • index c of table `test`.`t`,說明在等的是表 t 的索引 c 上面的鎖;
  • lock mode S waiting 表示這個語句要本身加一個讀鎖,當前的狀態是等待中;
  • Record lock 說明這是一個記錄鎖;
  • n_fields 2 表示這個記錄是兩列,也就是字段 c 和主鍵字段 id;
  • 0: len 4; hex 0000000a; asc ;; 是第一個字段,也就是 c。值是十六進制 a,也就是10;
  • 1: len 4; hex 0000000a; asc ;; 是第二個字段,也就是主鍵 id,值也是 10;
  • 這兩行裏面的 asc 表示的是,接下來要打印出值裏面的「可打印字符」,但 10 不是可打印字符,所以就顯示空格。
  • 第一個事務信息就只顯示出了等鎖的狀態,在等待 (c=10,id=10) 這一行的鎖。
  • 固然你是知道的,既然出現死鎖了,就表示這個事務也佔有別的鎖,可是沒有顯示出來。彆着急,咱們從第二個事務的信息中推導出

3. 第二個事務顯示的信息要多一些:

  • 「 HOLDS THE LOCK(S)」用來顯示這個事務持有哪些鎖;
  • index c of table `test`.`t` 表示鎖是在表 t 的索引 c 上;
  • hex 0000000a 和 hex 00000014 表示這個事務持有 c=10 和 c=20 這兩個記錄鎖;
  • WAITING FOR THIS LOCK TO BE GRANTED,表示在等 (c=5,id=5) 這個記錄鎖。

從上面這些信息中,咱們就知道:

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 語句,來回滾。

5、怎麼看鎖等待?

看完死鎖,咱們再來看一個鎖等待的例子。

在第 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 這裏有幾個信息:

  • insert intention 表示當前線程準備插入一個記錄,這是一個插入意向鎖。爲了便於理解,你能夠認爲它就是這個插入動做自己。
  • gap before rec 表示這是一個間隙鎖,而不是記錄鎖。

3. 那麼這個 gap 是在哪一個記錄以前的呢?接下來的 0~4 這 5 行的內容就是這個記錄的信息。

4. n_fields 5 也表示了,這一個記錄有 5 列:

  • insert intention 表示當前線程準備插入一個記錄,這是一個插入意向鎖。爲了便於理解,你能夠認爲它就是這個插入動做自己。
  • gap before rec 表示這是一個間隙鎖,而不是記錄鎖
  • 0: len 4; hex 0000000f; asc ;; 第一列是主鍵 id 字段,十六進制 f 就是 id=15。因此,這時咱們就知道了,這個間隙就是 id=15 以前的,由於 id=10 已經不存在了,它表示的就是 (5,15)。
  • 1: len 6; hex 000000000513; asc ;; 第二列是長度爲 6 字節的事務 id,表示最後修改這一行的是 trx id 爲 1299 的事務。
  • 2: len 7; hex b0000001250134; asc % 4;; 第三列長度爲 7 字節的回滾段信息。能夠看到,這裏的 acs 後面有顯示內容 (% 和 4),這是由於恰好這個字節是可打印字 符。後面兩列是 c 和 d 的值,都是 15。

所以,咱們就知道了,因爲 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] 鎖起來

也就是說,所謂「間隙」,其實根本就是由「這個間隙右邊的那個記錄」定義的。

6、update 的例子

看過了 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) 中插入數據,因此就被堵住了。

7、小結

今天這篇文章,我用前面第 20和第 21 篇文章評論區的幾個問題,再次跟你複習了加鎖規則。而且,我和你重點說明了,分析加鎖範圍時,必定要配合語句執行邏輯來進行。

在我看來,每一個想認真瞭解 MySQL 原理的同窗,應該都要可以作到:經過 explain 的結果,就可以腦補出一個 SQL 語句的執行流程。達到這樣的程度,纔算是對索引組織表、索
引、鎖的概念有了比較清晰的認識。你一樣也能夠用這個方法,來驗證本身對這些知識點的掌握程度。

在分析這些加鎖規則的過程當中,我也順便跟你介紹了怎麼看 show engine innodb status輸出結果中的事務信息和死鎖信息,但願這些內容對你之後分析現場能有所幫助。

老規矩,即使是答疑文章,我也仍是要留一個課後問題給你的。

上面咱們提到一個很重要的點:所謂「間隙」,其實根本就是由「這個間隙右邊的那個記錄」定義的。

那麼,一個空表有間隙嗎?這個間隙是由誰定義的?你怎麼驗證這個結論呢?

你能夠把你關於分析和驗證方法寫在留言區,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

8、上期問題時間

我在上一篇文章最後留給的問題,是分享一下你關於業務監控的處理經驗。

在這篇文章的評論區,不少同窗都分享了不錯的經驗。這裏,我就選擇幾個比較典型的留言,和你分享吧:

@老楊同志 回答得很詳細。他的主要思路就是關於服務狀態和服務質量的監控。其中,服務狀態的監控,通常均可以用外部系統來實現;而服務的質量的監控,就要經過接口
的響應時間來統計。

@Ryoma 同窗,提到服務中使用了 healthCheck 來檢測,其實跟咱們文中提到的select 1 的模式相似。

@強哥 同窗,按照監控的對象,將監控分紅了基礎監控、服務監控和業務監控,並分享了每種監控須要關注的對象。

這些都是很好的經驗,你也能夠根據具體的業務場景借鑑適合本身的方案。

相關文章
相關標籤/搜索