簡單瞭解 MySQL 中相關的鎖

detectiveHLH發表於2021-05-24

本文主要是帶大家快速瞭解 InnoDB 中鎖相關的知識

為什麼需要加鎖

首先,為什麼要加鎖?我想我不用多說了,想象接下來的場景你就能 GET 了。

你在商場的衛生間上廁所,此時你一定會做的操作是啥?鎖門。如果不鎖門,上廁所上著上著,啪一下門就被開啟了,可能大概也許似乎貌似有那麼一丁點的不太合適。

資料也是一樣,在併發的場景下,如果不對資料加鎖,會直接破壞資料的一致性,並且如果你的業務涉及到錢,那後果就更嚴重了。

鎖門表情包

鎖的分類

在 InnoDB 中,都有哪些鎖?其實你應該已經知道了很多了,例如面試中會問你儲存引擎 MyISAM 和 InnoDB 的區別,你會說 MyIASM 只有表鎖,但是 InnoDB 同時支援行鎖和表鎖。你可能還會被問到樂觀鎖和悲觀鎖的區別是啥。

鎖的概念、名詞很多,如果你沒有對鎖構建出一個完整的世界觀,那麼你理解起來就會比較有阻礙,接下來我們把這些鎖給分一下類。

按照鎖的粒度

按照鎖的粒度進行劃分可以分為:

  • 表鎖
  • 行鎖

這裡就不討論頁鎖了,頁鎖是 BDB(BerkeleyDB) 儲存引擎中才有的概念,我們這裡主要討論 InnoDB 儲存引擎。

按照鎖的思想

按照加鎖的思想可以分為:

  • 悲觀鎖
  • 樂觀鎖

這裡的悲觀、樂觀和你平時理解的名詞是同一個意思。樂觀鎖認為大概率不會發生衝突,只在必要的時候加鎖。而悲觀鎖認為大概率會衝突,所以無論是否必要加鎖都會執行加鎖操作。

按照相容性

按照相容性可以把鎖劃分為:

  • 共享鎖
  • 排他鎖

被加上共享鎖的資源,能夠和其他人進行共享,而如果被加上了排他鎖,其他人在拿不到這把鎖的情況下是無法進行任何操作的。

按照鎖的實現

這裡的實現就是 InnoDB 中具體的鎖的種類了,分別有:

  • 意向鎖(Intention Locks)
  • 記錄鎖(Record Locks)
  • 間隙鎖(Gap Locks)
  • 臨鍵鎖(Next-Key Locks)
  • 插入意向鎖(Insert Intention Locks)
  • 自增鎖(AUTO-INC Locks)

即使按照這種分類來對鎖進行了劃分,看到了這麼多的鎖的名詞可能仍然會有點懵。比如我SELECT ... FOR UPDATE 的時候到底加的是什麼鎖?

我們應該透過現象看本質,本質是什麼?本質是鎖到底加在了什麼物件上,而這個很好回答:

  • 加在了表上

  • 加在了行上

而對於加在行上的鎖,其本質又是什麼?本質是將鎖加在了索引上。

意向鎖

在 InnoDB 中支援了不同粒度的鎖,行鎖和表鎖。例如lock tables命令就會持有對應表的排他鎖。為了使多種不同粒度的鎖更實用,InnoDB 設計了意向鎖

意向鎖是一種表級鎖,它表明了接下來的事務中,會使用哪種型別的鎖,它有以下兩種型別:

  • 共享意向鎖(IS) 表明該事務會打算對錶中的記錄加共享鎖
  • 獨佔意向鎖(IX) 則是加排他鎖

例如,select ... for share就是加的共享意向鎖,而SELECT .. FOR UPDATE則是加的獨佔意向鎖。其規則如下:

  • 一個事務如果想要獲取某張表中某行的共享鎖,它必須先獲取該表的共享意向鎖,或者獨佔意向鎖。
  • 同理,如果想獲取排他鎖,它必須先獲取獨佔意向鎖

下圖是這幾種鎖的組合下相互互斥、相容的情況

對照上面的表,在相互相容的情況下,對應的事務就能獲取鎖,但是如果不相容則無法獲取鎖,直到不相容的鎖釋放之後才能獲取。

看到這裡你可能就會有問題了,那既然意向鎖除了 LOCK TBALES 之外什麼都不阻塞。那我要它何用?

還是通過例子,假設事務 A 獲取了 student 表中 id = 100 這行的共享鎖,之後事務 B 需要申請 student 表的排他鎖。而這兩把鎖明顯是衝突的,而且還是對於同一行。

那 InnoDB 需要如何感知 A 獲取了這把鎖?遍歷整個 B+ 樹嗎?不,答案就是意向鎖。事務 B 申請寫表的排他鎖時,InnoDB 會發現事務 A 已經獲取了該表的意向共享鎖,說明 student 表中已經有記錄被共享鎖鎖住了。此時就會阻塞住。

並且,意向鎖除了像LOCK TABLES這種操作之外,不會阻塞其他任何操作。換句話說,意向鎖只會和表級別的鎖之間發生衝突,而不會和行級鎖發生衝突。因為意向鎖的主要目的是為了表明有人即將、或者正在鎖定某一行。

就像你去圖書館找書,你並不需要每個書架挨著挨著找,直接去服務檯用電腦一搜,就知道圖書館有沒有這本書。

記錄鎖

這就是記錄鎖,是行鎖的一種。記錄鎖的鎖定物件是對應那行資料所對應的索引。對索引不太清楚的可以看看這篇文章

當我們執行SELECT * FROM student WHERE id = 1 FOR UPDATE語句時,就會對值為1的索引加上記錄鎖。至於要是一張表裡沒有索引該怎麼辦?這個問題在上面提到的文章中也解釋過了,當一張表沒有定義主鍵時,InnoDB 會建立一個隱藏的RowID,並以此 RowID 來建立聚簇索引。後續的記錄鎖也會加到這個隱藏的聚簇索引上。

當我們開啟一個事務去更新 id = 1 這行資料時,如果我們不馬上提交事務,然後再啟一個事務去更新 id = 1 的行,此時使用 show engine innodb status檢視,我們可以看到lock_mode X locks rec but not gap waiting的字樣。

X是排他鎖的意思,從這可以看出來,記錄鎖其實也可以分為共享鎖、排他鎖模式。當我們使用FOR UPDATE是排他,而使用LOCK IN SHARE MODE 則是共享。

而在上面字樣中出現的 gap 就是另一種行鎖的實現間隙鎖

間隙鎖

對於間隙鎖(Gap Locks)而言,其鎖定的物件也是索引。為了更好的瞭解間隙鎖,我們舉個例子。

SELECT name FROM student WHERE age BETWEEN 18 AND 25 FOR UPDATE

假設我們為 age 建立了非聚簇索引,執行該語句會阻止其他事務向 student 表中新增 18-25 的資料,無論表中是否真的有 age 為 18-25 的資料。因為間隙鎖的本質是鎖住了索引上的一個範圍,而 InnoDB 中索引在底層的B+樹上的儲存是有序的。

再舉個例子:

SELECT * FROM student WHERE age = 10 FOR UPDATE;

值得注意的是,這裡的 age 不是唯一索引,就是一個簡單的非聚簇索引。此時會給 age = 10 的資料加上記錄鎖,並且鎖定 age < 10 的 Gap。如果當前這個事務不提交,其他事務如果要插入一條 age < 10 的資料時,會被阻塞住。

間隙鎖是 MySQL 在對效能、併發綜合考慮之下的一種折中的解決方案,並且只在**可重複讀(RR)下可用,如果當前事務的隔離級別為讀已提交(RC)**時,MySQL會將間隙鎖禁用。

剛剛說了,記錄鎖分為共享、排他,間隙鎖其實也一樣。但是不同於記錄鎖的一點,共享間隙鎖、排他間隙鎖相互不互斥,這是怎麼回事?

我們還是需要透過現象看到本質,間隙鎖的目的是什麼?

為了防止其他事務在 Gap 中插入資料

那共享、排他間隙鎖在這個目標上是一致的,所以是可以同時存在的。

臨鍵鎖

臨鍵鎖(Next-Key Locks)是 InnoDB 最後一種行鎖的實現,臨鍵鎖實際上是記錄鎖間隙鎖的組合。換句話說,臨鍵鎖會給對應的索引加上記錄鎖,並且外加鎖定一個區間。

但是並不是所有臨鍵鎖都是這麼玩的,對於下面的SQL:

SELECT * FROM student WHERE id = 23;

在這種情況下,id是主鍵,唯一索引,無論其他事務插入了多少資料,id = 23這條資料永遠也只有一條。此時再加一個間隙鎖就完全沒有必要了,反而會降低併發。所以,在使用的索引是唯一索引的時候,臨鍵鎖會降級為記錄鎖

假設我們有10,20,30總共3條索引資料。那麼對應臨鍵鎖來說,可能鎖定的區間就會如下:

  • (∞, 10]
  • (10, 20]
  • (20, 30]
  • (30, ∞)

InnoDB 的預設事務隔離級別為可重複讀(RR),在這個情況下,InnoDB 就會使用臨鍵鎖,以防止幻讀的出現。

簡單解釋一下幻讀,就是在事務內,你執行了兩次查詢,第一次查詢出來 5 條資料,但是第二次再查,居然查出了 7 條資料,這就是幻讀

可能你在之前的很多部落格,或者面試八股文上,瞭解到過 InnoDB 的RR事務隔離級別可以防止幻讀,RR防止幻讀的關鍵就是臨鍵鎖

舉個例子,假設 student 表中就兩行資料,id分別為90和110.

SELECT * FROM student WHERE id > 100 FOR UPDATE;

當執行該 SQL 語句之後,InnoDB就會給區間 (90, 110] 和(110,∞) 加上間隙鎖,同時給 id=110 的索引加上記錄鎖。這樣以來,其他事務就無法向這個區間內新增資料,即使 100 根本不存在。

插入意向鎖

接下來是插入意向鎖(Insert Intention Locks),當我們執行 INSERT 語句之前會加的鎖。本質上是間隙鎖的一種。

還是舉個例子,假設我們現在有索引記錄10、20,事務A、B分別插入索引值為14、16的資料,此時事務A和B都會用插入意向鎖鎖住 10-20 之間的 Gap,獲取了插入意向鎖之後就會獲取14、16的排他鎖。

此時事務A和B是不會相互阻塞的,因為他們插入的是不同的行。

自增鎖

最後是自增鎖(AUTO-INC Locks),自增鎖的本質是表鎖,較為特殊。當事務 A 向包含了 AUTO_INCREMENT 列的表中新增資料時,就會持有自增鎖。而此時其他的事務 B 則必須要等待,以保證事務 A 取得連續的自增值,中間不會有斷層。

好了以上就是本篇部落格的全部內容了,歡迎微信搜尋關注【SH的全棧筆記】,回覆【佇列】獲取MQ學習資料,包含基礎概念解析和RocketMQ詳細的原始碼解析,持續更新中。

如果你覺得這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

相關文章