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

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

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

爲了方便你理解,我們再一起復習一下加鎖規則。這個規則中,包含了兩個“原則”、兩個“優化”和一個“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條件是不等號,這個過程裏也有等值查詢?

我們一起來看下這個例子,分析一下這條查詢語句的加鎖範圍:

begin;
select * from t where id>9 and id<12 order by id desc for update;

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

我們說加鎖單位是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=5、10、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),是最終的處理結果,表示回滾了第一個事務。
  1. 第一個事務的信息中:
  • 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)這一行的鎖。
  • 當然你是知道的,既然出現死鎖了,就表示這個事務也佔有別的鎖,但是沒有顯示出來。彆着急,我們從第二個事務的信息中推導出來。
  1. 第二個事務顯示的信息要多一些:
  • “ 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 表示這是一個間隙鎖,而不是記錄鎖。
  1. 那麼這個gap是在哪個記錄之前的呢?接下來的0~4這5行的內容就是這個記錄的信息。
  2. 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,suprenum]。

之後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的模式類似。
@強哥 同學,按照監控的對象,將監控分成了基礎監控、服務監控和業務監控,並分享了每種監控需要關注的對象。
這些都是很好的經驗,你也可以根據具體的業務場景借鑑適合自己的方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章