圖文例項解析,InnoDB 儲存引擎中行鎖的三種演算法

飛天小牛肉發表於2021-08-05

前文提到,對於 InnoDB 來說,隨時都可以加鎖(關於加鎖的 SQL 語句這裡就不說了,忘記的小夥伴可以翻一下上篇文章),但是並非隨時都可以解鎖。具體來說,InnoDB 採用的是兩階段鎖定協議(two-phase locking protocol):即在事務執行過程中,隨時都可以執行加鎖操作,但是只有在事務執行 COMMIT 或者 ROLLBACK 的時候才會釋放鎖,並且所有的鎖是在同一時刻被釋放。

並且,行級鎖只在儲存引擎層實現,而對於 InnoDB 儲存引擎來說,行級鎖又分三種,或者說有三種行級鎖演算法:

  • Record Lock:記錄鎖
  • Gap Lock:間隙鎖
  • Next-Key Lock:臨鍵鎖

下面,我們來詳細解釋下這三種行鎖演算法。

Record Lock 記錄鎖

顧名思義,記錄鎖就是為某行記錄加鎖,事實上,它封鎖的是該行的索引記錄。如果表在建立的時候沒有設定任何一個索引,那麼這時 InnoDB 儲存引擎會使用 “隱式的主鍵” 來進行鎖定。

所謂隱式的主鍵就是指:如果在建表的時候沒有指定主鍵,InnoDB 儲存引擎會將第一列非空的列作為主鍵;如果沒有的話會自動生成一列為 6 位元組的主鍵。

那麼,既然 Record Lock 是基於索引的,那如果我們的 SQL 語句中的條件導致索引失效(比如使用 or) 或者說條件根本就不涉及索引或者主鍵,行級鎖就將退化為表鎖。

Record Lock 示例

先來舉個對索引欄位進行查詢的例子,有資料庫如下,id 是主鍵索引:

CREATE TABLE `test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

初始資料是這樣的:

新建兩個事務,先執行事務 T1 的前兩行,也就是不要執行 commit

image-20210801215210231

由於沒有執行 commit,所以這個時候事務 T1 沒有釋放鎖,並且鎖住了 id = 1 的記錄行,此時再來執行事務 2 申請 id = 2 的記錄行:

image-20210801215329321

可以看見,由於鎖住的是不同的記錄行,所以兩個記錄鎖並沒有相互排斥,來看一下現在表中的資料,由於事務 1 還沒有 commit,所以應該是隻有 id = 2 的 username 被修改了:

image-20210801215624898

nice,果然。再執行下事務 1 的 commit,id = 1 的 username 也就被修改過來啦。

行鎖退化為表鎖示例

再來看下沒有使用索引的例子:

同樣的,新建兩個事務,先執行事務 T1 的前兩行,也就是不要執行 commit。我們試圖使用 select ... for update 給 username = "user_three" 的記錄行加上記錄鎖,但是由於 username 並非主鍵也並非索引,所以實際上這裡事務 T1 鎖住的是整張表:

image-20210801220807603

由於沒有執行 commit,所以這個時候事務 T1 沒有釋放鎖,並且鎖住了整張表。此時再來執行事務 2 試圖申請 id = 5 的記錄鎖,你會發現事務 T2 會卡住,最後超時關閉事務:

image-20210801221604790

兩條不同記錄擁有相同的索引,會發生鎖衝突嗎?

這個問題的答案應該很簡單吧,上面我們強調過,行鎖鎖住的是索引,而不是一條記錄(只不過我們平常這麼說鎖住了哪條記錄,比較好理解罷了)。所以如果兩個事務分別操作的兩條不同記錄擁有相同的索引,某個事務會因為行鎖被另一個事務佔用而發生等待

Gap Lock 間隙鎖

這裡我先簡單提一嘴,下文會詳細解釋:不同於 Record Lock 是基於唯一索引的,Gap Lock 和 Next-Key Lock 都是基於非唯一索引的。

並且,不同於 Record Lock 鎖定的是某一個索引記錄,Gap Lock 和 Next-Key Lock 鎖定的都是一段範圍內的索引記錄:

select * from test where id between 1 and 10 for update;

對於上述 SQL 語句,所有在(1,10)區間內(左開右開)的記錄行都會被 Gap Lock 鎖住,所有 id 為 2、3、4、5、6、7、8、9 的資料行的插入會被阻塞,但是 1 和 10 兩條被操作的索引記錄並不會被鎖住

注意!這裡指的是鎖住所有的(1,10)區間內的 id,也就是說即使某個 id 目前並不在我們的表中比如 id = 6 ,如果你想插入一條 id = 6 的新紀錄,那對不起,不行。

Next-Key Lock 臨鍵鎖

Next-Key Lock 是結合了 Gap Lock 和 Record Lock 的一種鎖定演算法,其主要目的是為了解決幻讀問題

例如一個索引有 10,11,13 和 20 這四個值,分別對這個 4 個索引進行加鎖操作,那麼這四個操作分別對應的 Next-Key Lock 鎖住的區間是:

  • (-∞, 10]
  • (10, 11]
  • (11, 13]
  • (13, 20]
  • (20, +∞]

細心的同學應該已經注意到了,和 Gap Lock 的不同之處就在於,Next-Key Lock 鎖定的區間是左開右閉的,也就是說它是包含當前被操作的索引記錄的。

在 InnoDB 預設的隔離級別 REPEATABLE-READ 下,行鎖預設使用的演算法就是 Next-Key Lock。但是,如果操作的索引是唯一索引或主鍵,InnoDB 會對 Next-Key Lock 進行優化,將其降級為 Record Lock,即僅鎖住索引本身,而不是範圍。

由於主鍵也是一種唯一索引,所以我們可以這麼說:Record Lock 是基於唯一索引的,而 Next-Key Lock 是基於非唯一索引的

需要注意的,當操作的索引為非唯一索引時,InnoDB 會先用 Record Lock 鎖住對應的唯一索引,再用 Next-Key Lock 和 Gap Lock 對這個非唯一索引進行處理,而不僅僅是鎖住這個非唯一索引。具體地我們舉個例子來看下。

Next-Key Lock 示例

假設我們為上面 test 表中新增一個欄位,並設定為非唯一索引:

CREATE TABLE `test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `class` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `index_class` (`class`) USING BTREE COMMENT '非唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

插入一些資料:

image-20210802225225160

開啟一個事務 1 執行如下的操作語句:

select * from test where class = 3 for update;

image-20210802225348249

在這種情況下,InnoDB 事實上會加上三種行鎖(select * ... from update 加的是行級寫鎖即 X 鎖):

1)給主鍵索引 id = 105 加上 Record Lock

2)對於非唯一索引 class = 3,其加上的是 Next-Key Lock,鎖定的範圍是 (1,3]

3)另外,特別需要注意的是,InnoDB 儲存引擎還會對非唯一索引 class 的下一個鍵值加上 Gap Lock(表中 class = 3 的下個鍵值是 6),所以還有一個 class 索引範圍為 (3,6) 的間隙鎖

總結下 2)和 3),對於這條 SQL 語句,InnoDB 儲存引擎鎖定地 class 索引範圍是 (1, 6)

下面我們用實踐來驗證理論,再開啟一個事務 2,執行下述的語句:

image-20210802225636814

不出所料,由於在事務 1 中執行的 SQL 語句已經對主鍵索引中列 a=105 的記錄加上了 X 鎖,所以此處再去獲取 這個記錄的 X 鎖會被阻塞住。

再用一個事務來執行下述 SQL 語句:

image-20210802230358942

主鍵插入 104 沒有任何問題,但是插入的 class 索引值 2 在被鎖定的範圍 (1,6) 中,因此執行同樣會被阻塞住。

經過上面的分析,大家一定能夠知道下面的 SQL 語句是可以正常執行的:

image-20210802230542969

Attention

需要注意的是,Next-Key Lock 降級為 Record Lock 僅存在於操作所有的唯一索引列的情況。若唯一索引由多個列組成,而操作的僅是多個唯一索引列中的其中一個,那麼 InnoDB 儲存引擎依然使用 Next-Key Lock 進行鎖定

? 關注公眾號 | 飛天小牛肉,即時獲取更新

  • 博主東南大學碩士在讀,攜程 Java 後臺開發暑期實習生,利用課餘時間運營一個公眾號『 飛天小牛肉 』,2020/12/29 日開通,專注分享計算機基礎(資料結構 + 演算法 + 計算機網路 + 資料庫 + 作業系統 + Linux)、Java 技術棧等相關原創技術好文。本公眾號的目的就是讓大家可以快速掌握重點知識,有的放矢。關注公眾號第一時間獲取文章更新,成長的路上我們一起進步
  • 並推薦個人維護的開源教程類專案: CS-Wiki(Gitee 推薦專案,現已累計 1.8k+ star), 致力打造完善的後端知識體系,在技術的路上少走彎路,歡迎各位小夥伴前來交流學習 ~ ?
  • 如果各位小夥伴春招秋招沒有拿得出手的專案的話,可以參考我寫的一個專案「開源社群系統 Echo」Gitee 官方推薦專案,目前已累計 900+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文件和配套教程。公眾號後臺回覆 Echo 可以獲取配套教程,目前尚在更新中。

相關文章