如何解決幻讀的
前言
大部分人在日常的業(yè)務(wù)開(kāi)發(fā)中,其實(shí)很少去關(guān)注數(shù)據(jù)庫(kù)的事務(wù)相關(guān)問(wèn)題,基本上都是 CURD 一把梭。正好最近在看 MySQL 的相關(guān)基礎(chǔ)知識(shí),其中對(duì)于幻讀問(wèn)題之前一直沒(méi)有理解深刻,今天就來(lái)聊聊「InnoDB 是如何解決幻讀的」,話不多說(shuō),下面進(jìn)入主題。
事務(wù)隔離級(jí)別
事務(wù)隔離是數(shù)據(jù)庫(kù)處理的基礎(chǔ)之一,是?ACID?中的?I
。在 MySQL 的 InnoDB 引擎中支持在?SQL:1992?標(biāo)準(zhǔn)中的四種事務(wù)隔離級(jí)別,如下圖所示,其中 P1 表示臟讀(Dirty read),P2 表示不可重復(fù)讀(Dirty read),P3 表示幻讀(Phantom)。
為什么需要定義這么多隔離呢?從上圖中也能猜出一二了,InnoDB 提供多個(gè)隔離級(jí)別主要原因是:讓使用者可以在多個(gè)事務(wù)同時(shí)進(jìn)行更改和執(zhí)行查詢時(shí)微調(diào)性能與結(jié)果的可靠性、一致性和可再現(xiàn)性之間的平衡的設(shè)置。是一種性能與結(jié)果可靠性間的?trade off
。
什么是幻讀
在聊「InnoDB 解決幻讀方式」前我們需要先了解幻讀是什么,官方文檔的描述如下:
A row that appears in the?result set?of a query, but not in the?result set?of an earlier query.
其中我加粗的「result set」是關(guān)鍵的地方,兩次查詢返回的是結(jié)果集,說(shuō)明必須是一個(gè)范圍查詢操作??偨Y(jié)下,幻讀就是:在同一個(gè)事務(wù)中,在前后兩次查詢相同范圍時(shí),兩次查詢得到的結(jié)果是不一致的。所以幻讀會(huì)產(chǎn)生數(shù)據(jù)一致性問(wèn)題。
InnoDB 解決幻讀方式
為了解決上述的幻讀問(wèn)題,InnoDB 引入了兩種鎖,分別是「間隙鎖」和「next-key 鎖」。下面通過(guò)一個(gè)示例來(lái)描述這兩種鎖的作用分別是什么。假如存在一個(gè)這樣的 B+ Tree 的索引結(jié)構(gòu),結(jié)構(gòu)中有 4 個(gè)索引元素分別是:9527、9530、9535、9540。

此時(shí)當(dāng)我們使用如下 SQL 通過(guò)主鍵索引查詢一條記錄,并且加上 X 鎖(排它鎖)時(shí):
select * from user where id = 9527 for update;
這時(shí)就會(huì)產(chǎn)生一個(gè)記錄鎖(也就是行鎖),鎖定?id = 9527
?這個(gè)索引。

在被鎖定的記錄(這里是 id = 9527)的鎖釋放之前,其它事務(wù)無(wú)法對(duì)這條被鎖定記錄做任何操作。再回憶一下,前面說(shuō)的幻讀定義「在同一個(gè)事務(wù)中,在前后兩次查詢相同范圍時(shí),兩次查詢得到的結(jié)果是不一致」。注意,這里強(qiáng)調(diào)的是范圍查詢。
InnoDB 要解決幻讀問(wèn)題,就必須得保證在如果在一個(gè)事務(wù)中,通過(guò)如下這條語(yǔ)句進(jìn)行鎖定時(shí):
select * from user where id > 9530 and id < 9535 for update;
此時(shí),另外一個(gè)語(yǔ)句再執(zhí)行一如下這條 insert 語(yǔ)句時(shí),需要被阻塞,直到上面這個(gè)獲得鎖的事務(wù)釋放鎖后才能執(zhí)行。
insert into user(id, name, age) values(9533, 'Jack', 44);
為此,InnoDB 引入了「間隙鎖」,它的主要功能是鎖定一段范圍內(nèi)的索引記錄。比如上面查詢?id > 9530 and id < 9535
?的時(shí)候,對(duì) B+ Tree 中的(9530,9535)這個(gè)開(kāi)區(qū)間范圍的索引加間隙鎖。
在這種加了間隙鎖的情況下,其它事務(wù)對(duì)這個(gè)區(qū)間的數(shù)據(jù)進(jìn)行插入、更新、刪除都會(huì)被鎖住直到這個(gè)獲取到鎖的事務(wù)釋放。

這種是在區(qū)間之間的情況,你可能想到另外的一種情況:鎖定多個(gè)區(qū)間,如下的一條語(yǔ)句:
select * from user where id > 9530 for update;
上面這條查詢語(yǔ)句是針對(duì)?id > 9530
?這個(gè)條件加鎖,那么此時(shí)它需要鎖定多個(gè)索引區(qū)間,所以在這種情況下 InnoDB 引入了「next-key 鎖」機(jī)制。其實(shí) next-key 鎖的效果相當(dāng)于間隙鎖和記錄鎖的合集,記錄鎖鎖定存在的記錄行,間隙鎖鎖住記錄行之間的間隙,而 next-key 鎖它鎖住的是兩者之和。

在 InnoDB 中,每個(gè)數(shù)據(jù)行上的非唯一索引列上都會(huì)存在一把 next-key 鎖,當(dāng)某個(gè)事務(wù)持有該數(shù)據(jù)行的 next-key 鎖時(shí),會(huì)鎖住一段左開(kāi)右閉區(qū)間的數(shù)據(jù)。因此,當(dāng)通過(guò)?id > 9530
?這樣一種范圍查詢加鎖時(shí),會(huì)加 next-key 鎖,鎖定區(qū)間是范圍是:
(9530,9535] (9535,9540] (9540,+∞]

間隙鎖(也叫 Gap 鎖)和 next-key 鎖的區(qū)別在于加鎖的范圍,間隙鎖只鎖定兩個(gè)索引之間的引用間隙,而 next-key 鎖會(huì)鎖定多個(gè)索引區(qū)間,它包含「記錄鎖」和「間隙鎖」。所以,當(dāng)我們使用了范圍查詢,不僅僅命中了已存在的 Record 記錄,還包含了 Gap 間隙。