記錄鎖、間隙鎖與 Next-Key Lock
有小伙伴在微信上表示面試時(shí)被問(wèn)到了 Next-Key Lock 是啥,結(jié)果一臉懵逼,那么今天我們來(lái)捋一捋 MySQL 中的記錄鎖、間隙鎖以及 Next-Key Lock。
1. Record LockRecord
Lock 也就是我們所說(shuō)的記錄鎖,記錄鎖是對(duì)索引記錄的鎖,注意,它是針對(duì)索引記錄,即它只鎖定記錄這一行數(shù)據(jù)。
例如如下一條 SQL:
select * from user where id=1 for update;
注意,id 是索引,id 如果不是索引,上面這條 SQL 所加的排他鎖就不是一個(gè) Record Lock。
我們來(lái)看如下一個(gè)例子:
首先我們將系統(tǒng)變量 innodb_status_output_locks 設(shè)置為 ON,如下:
接下來(lái)我們執(zhí)行如下 SQL,鎖定一行數(shù)據(jù),此時(shí)會(huì)自動(dòng)為表加上 IX 鎖:
接下來(lái)我們?cè)谝粋€(gè)新的會(huì)話中執(zhí)行如下指令來(lái)查看 InnoDB 存儲(chǔ)引擎的情況:
show engine innodb status\G
輸出的信息很多,我們重點(diǎn)關(guān)注 TRANSACTIONS,如下:
可以看到:
- TABLE LOCK table test08.user trx id 3564804 lock mode IX:這句就是說(shuō)事務(wù) id 為 3564804 的事務(wù),為 user 表添加了意向排他鎖(IX)。
- RECORD LOCKS space id 851 page no 3 n bits 80 index PRIMARY of table test08.user trx id 3564804 lock_mode X locks rec but not gap:這個(gè)就是一個(gè)鎖結(jié)構(gòu)的記錄,這里的索引是 PRIMARY,加的鎖也是正兒八經(jīng)的記錄鎖(not gap)。
看到了 LOCKS REC BUT NOT GAP,就說(shuō)明這是一個(gè)記錄鎖。
那么這個(gè) Record Lock 和我們之前所講的 S 鎖以及 X 鎖有什么區(qū)別呢?S 鎖是共享鎖,X 鎖是排他鎖,當(dāng)我們加 S 鎖或者 X 鎖的時(shí)候,如果用到了索引,鎖加在了某一條具體的記錄上,那么這個(gè)鎖也是一個(gè)記錄鎖(其實(shí),記錄鎖,S 鎖,X 鎖,概念有一些重復(fù)的地方,但是描述的重點(diǎn)不一樣)。
或者也可以理解為記錄鎖又細(xì)分為 S 鎖和 X 鎖,它們之間的兼容性如下圖:
兼容性 | S 型記錄鎖 | X 型記錄鎖 |
S 型記錄鎖 | 兼容 | 不兼容 |
X 型記錄鎖 | 不兼容 | 不兼容 |
2. Gap Lock
Gap Lock 也叫做間隙鎖,它的存在可以解決幻讀問(wèn)題,另外需要注意,Gap Lock 也只在 REPEATABLE READ 隔離級(jí)別下有效。先來(lái)看看什么是幻讀,我們來(lái)看如下一個(gè)表格:
有兩個(gè)會(huì)話,A 和 B,先在會(huì)話 A 中開(kāi)啟事務(wù),然后查詢 age 為 99 的用戶總數(shù),注意使用當(dāng)前讀,因?yàn)樵谀J(rèn)的隔離級(jí)別下,默認(rèn)的快照讀并不能讀到其他事務(wù)提交的數(shù)據(jù),至于快照讀和當(dāng)前讀的區(qū)別,大家參考:S 鎖與 X 鎖,當(dāng)前讀與快照讀!。當(dāng)會(huì)話 A 中第一次查詢過(guò)后,會(huì)話 B 中向數(shù)據(jù)庫(kù)添加了一行記錄,等到會(huì)話 A 中第二次查詢的時(shí)候,就查到了和第一次查詢不一樣的結(jié)果,這就是幻讀(注意幻讀專指數(shù)據(jù)插入引起的不一致)。
在 MySQL 默認(rèn)的隔離級(jí)別 REPEATABLE READ 下,上圖所描述的情況無(wú)法復(fù)現(xiàn)。無(wú)法復(fù)現(xiàn)的原因在于,在 MySQL 的 REPEATABLE READ 隔離級(jí)別中,它已經(jīng)幫我們解決了幻讀問(wèn)題,解決的方案就是 Gap Lock。
大家想想,之所以出現(xiàn)幻讀的問(wèn)題,是因?yàn)橛涗浿g存在縫隙,用戶可以往這些縫隙中插入數(shù)據(jù),這就導(dǎo)致了幻讀問(wèn)題,如下圖:
如圖所示,id 之間有縫隙,有縫隙就有漏洞。前面我們所說(shuō)的記錄鎖只能鎖住一條具體的記錄,但是對(duì)于記錄之間的空隙卻無(wú)能無(wú)力,這就導(dǎo)致了幻讀(其他事務(wù)可往縫隙中插入數(shù)據(jù))。
現(xiàn)在 Gap Lock 間隙鎖,就是要把這些記錄之間的間隙也給鎖住,間隙鎖住了,就不用擔(dān)心幻讀問(wèn)題了,這也是 Gap Lock 存在的意義。
給一條記錄加 Gap Lock,是鎖住了這條記錄前面的空隙,例如給 id 為 1 的記錄加 Gap Lock,鎖住的范圍是 (-∞,1),給 id 為 3 的記錄加 Gap Lock,鎖住的范圍是 (1,3),那么 id 為 10 后面的空隙怎么鎖定呢?MySQL 提供了一個(gè) Supremum 表示當(dāng)前頁(yè)面中的最大記錄,所以最后針對(duì) Supremum 鎖住的范圍就是 (10,+∞),這樣,所有的間隙都被覆蓋到了,由于鎖定的是間隙,所以都是開(kāi)區(qū)間。
那么我們?cè)趺礃幽芸吹?Gap Lock 呢?我給大家舉一個(gè)簡(jiǎn)單的例子,假設(shè)我有如下一張表:
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
一個(gè)簡(jiǎn)單的表,id 是主鍵,age 是普通索引,表中有如下幾條記錄:
接下來(lái)我們執(zhí)行如下 SQL,鎖定一行數(shù)據(jù),此時(shí)也會(huì)產(chǎn)生間隙鎖:
接下來(lái)我們?cè)谝粋€(gè)新的會(huì)話中執(zhí)行如下指令來(lái)查看 InnoDB 存儲(chǔ)引擎的情況:
show engine innodb status\G
輸出的信息很多,我們重點(diǎn)關(guān)注 TRANSACTIONS,如下:
紅色框選中的,就是一個(gè)間隙鎖的加鎖記錄,可以看到,在某一個(gè)記錄之前加了間隙鎖。
這就是間隙鎖。非常重要的一點(diǎn)需要大家牢記:Gap Lock 只在 REPEATABLE READ 隔離級(jí)別下有效。
3. Next-Key Lock
以下內(nèi)容都是基于 MySQL 默認(rèn)的隔離級(jí)別 REPEATABLE READ。
如果我們既想鎖定一行,又想鎖定行之間的記錄,那么就是 Next-Key Lock 了,換言之,Next-Key Lock 是 Record Lock 和 Gap Lock 的結(jié)合體。
正常來(lái)說(shuō),我們加行鎖的基本單位就是 Next-Key Lock,即既有記錄鎖又有間隙鎖,但是有時(shí)候 Next-Key Lock 會(huì)退化,我們通過(guò)幾個(gè)簡(jiǎn)單的例子來(lái)分析一下。
首先我們來(lái)看看 Next-Key Lock 的加鎖規(guī)則:
- 鎖的范圍是左開(kāi)右閉。
- 如果是唯一非空索引的等值查詢,Next-Key Lock 會(huì)退化成 Record Lock。
- 普通索引上的等值查詢,向后遍歷時(shí),最后一個(gè)不滿足等值條件的時(shí)候,Next-Key Lock 會(huì)退化成 Gap Lock。
我們通過(guò)幾個(gè)簡(jiǎn)單的例子來(lái)分析下。
3.1 唯一非空索引
假設(shè)我有一個(gè)學(xué)生表,學(xué)生表中有學(xué)生的姓名和成績(jī),如下:
CREATE TABLE `student` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`score` double NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `score` (`score`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
id 是主鍵,score 是成績(jī),其中 score 是唯一非空索引。
現(xiàn)在表中有如下數(shù)據(jù):
假設(shè)我們執(zhí)行如下 SQL:
在這個(gè)例子中,由于 score 是唯一非空索引,所以 Next-Key Lock 會(huì)退化成 Record Lock,換句話說(shuō),這行 SQL 只給 score 為 90 的記錄加鎖,不存在 Gap Lock,即我們新開(kāi)一個(gè)會(huì)話,插入一條 score 為 88 的記錄也是 OK 的。
不過(guò)這里有一個(gè)特例,如果鎖定的是一個(gè)不存在的記錄,那么也會(huì)產(chǎn)生間隙鎖,例如下面這個(gè):
由于并不存在 score 為 91 的記錄,所以這里會(huì)產(chǎn)生一個(gè)范圍為 (90,95) 的間隙鎖,我們執(zhí)行如下 SQL 可以驗(yàn)證:
可以看到,90.1、94.9 都會(huì)被阻塞(我按了 Ctrl C,所以大家看到查詢終止)。
90、95 則不符合唯一非空索引的條件。
95.1 則可以插入成功。
沒(méi)問(wèn)題。
3.2 非空索引
現(xiàn)在我們重新開(kāi)始,將 score 索引改為普通索引,如下:
CREATE TABLE `student` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`score` double NOT NULL,
PRIMARY KEY (`id`),
KEY `score` (`score`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
數(shù)據(jù)還是跟前面一樣,此時(shí)我們來(lái)執(zhí)行如下 SQL:
我們來(lái)分析下。
此時(shí)要鎖定的是 id 為 90 的記錄,那么首先加間隙鎖,上一個(gè) score 為 89,所以這次加的間隙鎖范圍是 (89,90),同時(shí)要鎖定 id 為 90 的記錄,所以進(jìn)一步優(yōu)化為 (89,90]。
同時(shí),這里還有一條規(guī)則,就是滿足條件的上一條記錄,也需要被鎖住,所以最終的鎖范圍就是 [89,90]。
由于 score 不是唯一性索引,所以還需要繼續(xù)向后查找,找到的下一條記錄是 95,由于此時(shí) Next-Key Lock 會(huì)退化成 Gap Lock,所以鎖定的范圍是 (90,95)。綜上,最終鎖定的范圍是 [89,95)。
接下來(lái)我們可以新開(kāi)一個(gè)會(huì)話,我們分別嘗試添加如下數(shù)據(jù)看看是否能夠添加成功:
可以看到,score 為 88 是可以的,但是為 89.1 就不行。
score 為 95 也是可以的,但是為 94.9 就不行。
再試一下 89 是否可以:
說(shuō)明我們上面分析的加鎖范圍是正確的。
再來(lái)看如下一條 SQL:
跟前面的案例相比,這次多了 limit 1,limit 1 表示只要一條記錄,所以這次查找到 90 之后就不會(huì)再往后查找了,那么最終的鎖就是間隙鎖+一個(gè)記錄鎖,最終的范圍就是 [89,90]。
此時(shí)新開(kāi)一個(gè)會(huì)話,分別插入 score 為 88.9、89、90、91 的 記錄,驗(yàn)證我們上面所分析的加鎖范圍:
88.9 和 89 的插入結(jié)果跟我們預(yù)想的一致。
可以看到,這里 90 也能插入,能插入的原因是因?yàn)槿狈?90 往后的間隙鎖。
4. 小結(jié)
MySQL 中的鎖有點(diǎn)繁雜,小伙伴們可以趁著某個(gè)周末,花點(diǎn)時(shí)間捋一捋,以后面試再遇到這些問(wèn)題的時(shí)候就不頭大了。