30 | 答疑文章(二):用動態的觀點看加鎖

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

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

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

  • 原則1:加鎖的基本單位是next-key lock。但願你還記得,next-key lock是前開後閉區間。
  • 原則2:查找過程當中訪問到的對象纔會加鎖。
  • 優化1:索引上的等值查詢,給惟一索引加鎖的時候,next-key lock退化爲行鎖。
  • 優化2:索引上的等值查詢,向右遍歷時且最後一個值不知足等值條件的時候,next-key lock退化爲間隙鎖。
  • 一個bug:惟一索引上的範圍查詢會訪問到不知足條件的第一個值爲止。

接下來,咱們的討論仍是基於下面這個表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);

不等號條件裏的等值查詢

有同窗對「等值查詢」提出了疑問:等值查詢和「遍歷」有什麼區別?爲何咱們文章的例子裏面,where條件是不等號,這個過程裏也有等值查詢?spa

咱們一塊兒來看下這個例子,分析一下這條查詢語句的加鎖範圍:線程

begin;
select * from t where id>9 and id

利用上面的加鎖規則,咱們知道這個語句的加鎖範圍是主鍵索引上的 (0,5]、(5,10]和(10, 15)。也就是說,id=15這一行,並無被加上行鎖。爲何呢?3d

咱們說加鎖單位是next-key lock,都是前開後閉區間,可是這裏用到了優化2,即索引上的等值查詢,向右遍歷的時候id=15不知足條件,因此next-key lock退化爲了間隙鎖 (10, 15)。code

可是,咱們的查詢語句中where條件是大於號和小於號,這裏的「等值查詢」又是從哪裏來的呢?對象

要知道,加鎖動做是發生在語句執行過程當中的,因此你在分析加鎖行爲的時候,要從索引上的數據結構開始。這裏,我再把這個過程拆解一下。blog

如圖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 死鎖現場

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

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

    • (1) TRANSACTION,是第一個事務的信息;
    • (2) 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語句,來回滾。

怎麼看鎖等待?

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

在第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列:

    • 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<=20 order by c desc lock in share mode; 向左掃描到c=10的時候,要把(5, 10]鎖起來。

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

update的例子

看過了insert和delete的加鎖例子,咱們再來看一個update語句的案例。在留言區中@信信 同窗作了這個試驗:

圖 6 update 的例子

你能夠本身分析一下,session A的加鎖範圍是索引c上的 (5,10]、(10,15]、(15,20]、(20,25]和(25,supremum]。

注意:根據c>5查到的第一個記錄是c=10,所以不會加(0,5]這個next-key lock。

以後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所示的樣子:

圖 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的模式相似。
  • @強哥 同窗,按照監控的對象,將監控分紅了基礎監控、服務監控和業務監控,並分享了每種監控須要關注的對象。

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

相關文章
相關標籤/搜索