1. 程式人生 > >MySQL探祕(七):InnoDB行鎖演算法

MySQL探祕(七):InnoDB行鎖演算法

 在上一篇《InnoDB一致性非鎖定讀》中,我們瞭解到InnoDB使用一致性非鎖定讀來避免在一般的查詢操作(SELECT FOR UPDATE等除外)時使用鎖。然而鎖這個事情是無法避免的,資料的寫入,修改和刪除都需要加鎖。今天我們就繼續學習InnoDB鎖相關的知識。

 由於文章涉及的概念比較多,害怕大家看完後會罵人,有一種字我都認識,就不太懂的感覺,文章會給出一些例項和試驗,依據具體案例來講解這些概念。畢竟,實踐才能出真知。

 InnoDB儲存引擎支援表鎖和行鎖。顧名思義,表鎖是鎖住整張表,行鎖只是鎖住某些行。InnoDB通過給索引項加鎖來實現行鎖,如果沒有索引,則通過隱藏的聚簇索引來對記錄加鎖。如果操作不通過索引條件檢索資料,InnoDB 則對錶中的所有記錄加鎖,實際效果就和表鎖一樣。InnoDB儲存引擎有3種行鎖的演算法,分別是:

  • Record Lock: 單個記錄上的鎖
  • Gap Lock: 間隙鎖,鎖定一個範圍,但不包括記錄本上
  • Next-Key Lock: Gap Lock+Record Lock,鎖定一個範圍,並且鎖定記錄本身

 如下圖所示,

三種鎖演算法三種鎖演算法

 例如一個索引有10,11,13,20這四個值。InnoDB可以根據需要使用Record Lock將10,11,13,20四個索引鎖住,也可以使用Gap Lock將(-∞,10),(10,11),(11,13),(13,20),(20, +∞)五個範圍區間鎖住。Next-Key Locking類似於上述兩種鎖的結合,它可以鎖住的區間有為(-∞,10],(10,11],(11,13],(13,20],(20, +∞),可以看出它即鎖定了一個範圍,也會鎖定記錄本身。

 InnoDB儲存引擎的鎖演算法的一些規則如下所示,後續章節會給出對應的實驗案例和詳細講解。

  • 在不通過索引條件查詢時,InnoDB 會鎖定表中的所有記錄。所以,如果考慮效能,WHERE語句中的條件查詢的欄位都應該加上索引。

  • InnoDB通過索引來實現行鎖,而不是通過鎖住記錄。因此,當操作的兩條不同記錄擁有相同的索引時,也會因為行鎖被鎖而發生等待。

  • 由於InnoDB的索引機制,資料庫操作使用了主鍵索引,InnoDB會鎖住主鍵索引;使用非主鍵索引時,InnoDB會先鎖住非主鍵索引,再鎖定主鍵索引。

  • 當查詢的索引是唯一索引(不存在兩個資料行具有完全相同的鍵值)時,InnoDB儲存引擎會將Next-Key Lock降級為Record Lock,即只鎖住索引本身,而不是範圍。

  • InnoDB對於輔助索引有特殊的處理,不僅會鎖住輔助索引值所在的範圍,還會將其下一鍵值加上Gap LOCK。

  • InnoDB使用Next-Key Lock機制來避免Phantom Problem(幻讀問題)。

真的瞭解本質嗎?

 在不通過索引條件查詢時,InnoDB 會鎖定表中的所有記錄。大家可以登入上自己的MySQL伺服器,親自試驗一下。

示例一示例一

 試驗發現,會話二的查詢操作真的是會發生等待。那麼,這句話真的是對的嗎?我們可以使用《InnoDB鎖的型別和狀態查詢》中查詢資料鎖的方法查詢一下,注意必須在會話二操作還在等待時進行查詢,否則查詢不到

查詢鎖資訊查詢鎖資訊

 其中lock_trx_id為1851的事務是會話二的事務,另一個是會話一的事務。我們可以看到兩個鎖都要對值為1的主鍵索引加鎖。需要注意的是,這裡是對主鍵進行加鎖。二者之間的關係是怎麼確定的呢?我們可以通過information_schema.INNODB_LOCK_WAITS中的資料確定。

 奇怪,不是說好的鎖定表中的所有記錄嘛?查找了很多資料,發現INNODB_LOCKS的定義如下:

The INNODB_LOCKS table contains information about each lock that an InnoDB transaction has requested but not yet acquired, and each lock that a transaction holds that is blocking another transaction.

 也就是說,這張表並不會顯示所有鎖的資訊,而是隻顯示要申請卻沒有申請到,和已經持有鎖並且阻塞其他執行緒的鎖資訊。怪不得必須在會話二進行等待時進行查詢才能查得到資料。

 因為兩個會話的操作都要鎖住所有的行,所以發現每次在第一行記錄上就發生了鎖等待。那我們使用插入語句試試。表e1的主鍵a的值為1-4,我們分別插入主鍵為1-4(當然會有主鍵重複問題,但是由於有鎖,一直等待)的新記錄,分別查詢鎖資訊,就能看到會話一的事務對所有的主鍵都加了鎖,也就是對所有的記錄都加了鎖。

是索引,而不是記錄

 InnoDB儲存引擎的行鎖是通過鎖住索引實現的,而不是記錄。這是理解很多資料庫鎖問題的關鍵。

 由於InnoDB特殊的索引機制,資料庫操作使用主鍵索引時,InnoDB會鎖住主鍵索引;使用非主鍵索引時,InnoDB會先鎖住非主鍵索引,再鎖定主鍵索引。不瞭解InnoDB索引機制的可以參考這篇文章

 如下圖所示,當InnoDB鎖定非主鍵索引b時,它也會鎖住其對應的主鍵索引,所以鎖住b值為2和3的非主鍵索引,那麼與其相關的a值為6,5的主鍵索引也需要被鎖住。

非主鍵索引的加鎖非主鍵索引的加鎖

 比如說,一種常見的死鎖情況一般出現在如下圖所示的操作場景中。

示例2示例2

 會話一的語句使用了b上的索引,因為它是非主鍵索引,所以會先在b索引上新增鎖,再去a索引上加鎖。而會話二的語句恰恰相反,會先在索引a上加鎖,再去索引b加鎖。這種情況下,就可能出現死鎖。

Next-Key Lock鎖到底有什麼用?

 預設隔離級別REPEATABLE-READ下,InnoDB中行鎖預設使用演算法Next-Key Lock,只有當查詢的索引是唯一索引或主鍵時,InnoDB會對Next-Key Lock進行優化,將其降級為Record Lock,即僅鎖住索引本身,而不是範圍。當查詢的索引為輔助索引時,InnoDB則會使用Next-Key Lock進行加鎖。InnoDB對於輔助索引有特殊的處理,不僅會鎖住輔助索引值所在的範圍,還會將其下一鍵值加上Gap LOCK。

 廢話不多說,我們來看一下相關的實驗,先做一下準備。

CREATE TABLE e4 (a INT, b INT, PRIMARY KEY(a), KEY(b));
INSERT INTO e4 SELECT 1,1;
INSERT INTO e4 SELECT 3,1;
INSERT INTO e4 SELECT 5,3;
INSERT INTO e4 SELECT 7,6;
INSERT INTO e4 SELECT 10,8;

 然後開啟一個會話執行下面的語句。

SELECT * FROM e4 WHERE b=3 FOR UPDATE

 因為通過索引b來進行查詢,所以InnoDB會使用Next-Key Lock進行加鎖,並且索引b是非主鍵索引,所以還會對主鍵索引a進行加鎖。對於主鍵索引a,僅僅對值為5的索引加上Record Lock(因為之前的規則)。而對於索引b,需要加上Next-Key Lock索引,鎖定的範圍是(1,3]。除此之外,還會對其下一個鍵值加上Gap Lock,即還有一個範圍為(3,6)的鎖。
大家可以再新開一個會話,執行下面的SQL語句,會發現都會被阻塞。

SELECT * FROM e4 WHERE a = 5 FOR UPDATE;  # 主鍵a被鎖
INSERT INTO e4 SELECT 4,2;   # 插入行b的值為2,在鎖定的(1,3]範圍內
INSERT INTO e4 SELECT 6,5# 插入行b的值為5,在鎖定的(3,6)範圍內

 InnoDB引擎採用Next-Key Lock來解決幻讀問題。因為Next-Key Lock是鎖住一個範圍,所以就不會產生幻讀問題。但是需要注意的是,InnoDB只在Repeatable Read隔離級別下使用該機制。

後記

 我們後續還會探討InnoDB的事務的知識,請大家持續關注。

參考