InnoDB的RR到底有沒有解決幻讀?
在InnoDB中,Repeatable Read(重復(fù)讀)隔離級(jí)別通過間隙鎖和MVCC機(jī)制解決了大部分的幻讀問題,但并非所有幻讀都能被解決。要徹底解決幻讀,需要使用Serializable(可串行化)隔離級(jí)別。
在Repeatable Read隔離級(jí)別下,通過間隙鎖解決了部分當(dāng)前讀導(dǎo)致的幻讀問題。通過添加間隙鎖來鎖定記錄之間的間隙,以防止新數(shù)據(jù)的插入。
在Repeatable Read隔離級(jí)別下,通過MVCC機(jī)制解決了快照讀導(dǎo)致的幻讀問題。在該隔離級(jí)別下,進(jìn)行快照讀時(shí)僅在第一次進(jìn)行數(shù)據(jù)查詢,隨后直接讀取快照,因此不會(huì)發(fā)生幻讀。
然而,若兩個(gè)事務(wù)操作如下:事務(wù)1首先進(jìn)行快照讀,然后事務(wù)2插入一條記錄并提交,在事務(wù)1之后通過更新操作這個(gè)新插入的記錄,這樣可以成功更新,這就是幻讀的一種情況。
另外一個(gè)場(chǎng)景是,若兩個(gè)事務(wù)的順序?yàn)椋菏聞?wù)1先進(jìn)行快照讀,接著事務(wù)2插入了一條記錄并提交,在事務(wù)1進(jìn)行當(dāng)前讀后,再次進(jìn)行快照讀也會(huì)導(dǎo)致幻讀的發(fā)生。
MVCC解決幻讀
MVCC,即多版本并發(fā)控制(Multiversion Concurrency Control),類似于數(shù)據(jù)庫鎖,是一種并發(fā)控制的解決方案。它主要用于解決讀-寫并發(fā)的情況。
我們了解,在MVCC中存在兩種讀取方式:快照讀和當(dāng)前讀。
快照讀指的是讀取快照數(shù)據(jù),即在生成快照的那一瞬間的數(shù)據(jù)。例如,通常情況下我們使用的普通SELECT語句在不加鎖的情況下就是一種快照讀。
在可重復(fù)讀(RC)中,每次讀取都會(huì)重新生成一個(gè)快照,始終讀取行的最新版本。在可重復(fù)讀(RR)中,快照會(huì)在事務(wù)第一次執(zhí)行SELECT語句時(shí)生成,只有在本事務(wù)中對(duì)數(shù)據(jù)進(jìn)行更改才會(huì)更新快照。
因此,在RR隔離級(jí)別下,同一事務(wù)中的多次查詢不會(huì)檢索到其他事務(wù)的更改內(nèi)容,因此能夠解決幻讀問題。
若我們將事務(wù)隔離級(jí)別設(shè)置為RR,由于MVCC的機(jī)制,就可以解決幻讀問題。
有這樣一張表:
CREATE TABLE users (
id INT UNSIGNED AUTO_INCREMENT,
gmt_create DATETIME NOT NULL,
age INT NOT NULL,
name VARCHAR(16) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
INSERT INTO users(gmt_create,age,name) values(now(),18,'Paidaxing');
INSERT INTO users(gmt_create,age,name) values(now(),28,'Paidaxing2023');
INSERT INTO users(gmt_create,age,name) values(now(),38,'Paidaxing666');
執(zhí)行如下事務(wù)時(shí)序:
事務(wù)1 | |
SET session TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
BEGIN; | |
SELECT * FROM users WHERE AGE > 10 AND AGE <30; | |
BEGIN; | |
INSERT INTO users(gmt_create, age, name) values(now(), 20, 'Paidaxing999'); | |
COMMIT; | |
SELECT * FROM users WHERE AGE > 10 AND AGE < 30; |
可以觀察到,在同一個(gè)事務(wù)中,兩次查詢的結(jié)果是相同的。在可重復(fù)讀(RR)級(jí)別下,由于采用了快照讀,第二次查詢實(shí)際上是讀取的快照數(shù)據(jù)。
間隙鎖與幻讀
我們已經(jīng)討論了MVCC如何解決了可重復(fù)讀(RR)級(jí)別下的快照讀造成的幻讀問題,那么在當(dāng)前讀?。≧EAD COMMITTED)下,如何解決幻讀問題呢?
當(dāng)前讀取即讀取最新數(shù)據(jù),因此,鎖定的SELECT語句,或者進(jìn)行數(shù)據(jù)的插入、刪除、更新都屬于當(dāng)前讀取操作,例如:
SELECT * FROM xx_table LOCK IN SHARE MODE;
SELECT * FROM xx_table FOR UPDATE;
INSERT INTO xx_table ...
DELETE FROM xx_table ...
UPDATE xx_table ...
舉一個(gè)下面的例子:
事務(wù)1 | 事務(wù)2 |
SET session TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
BEGIN; | |
SELECT * FROM users WHERE AGE > 10 AND AGE < 30 for update; | |
BEGIN; | |
INSERT INTO users(gmt_create, age, name) values(now(), 20, 'Paidaxing999'); | |
阻塞 |
在可重復(fù)讀(RR)級(jí)別下,當(dāng)我們使用SELECT … FOR UPDATE時(shí),會(huì)進(jìn)行鎖定操作。這不僅會(huì)對(duì)行記錄進(jìn)行加鎖,還會(huì)對(duì)記錄之間的間隙進(jìn)行加鎖,這就是所謂的間隙鎖。
由于記錄之間的間隙被鎖定,事務(wù)2的插入操作被阻塞,直到事務(wù)1釋放鎖才得以成功執(zhí)行。
由于事務(wù)2無法成功插入數(shù)據(jù),因此幻讀現(xiàn)象得以避免。因此,在可重復(fù)讀(RR)級(jí)別中,通過引入間隙鎖的方式,成功規(guī)避了幻讀現(xiàn)象的發(fā)生。
解決不了的幻讀
前面我們討論了快照讀(無鎖查詢)和當(dāng)前讀(有鎖查詢)是如何解決幻讀問題的。然而,上面提到的例子并非幻讀的全部情況。
我們知道MVCC只能解決快照讀導(dǎo)致的幻讀問題,那么如果一個(gè)事務(wù)中發(fā)生了當(dāng)前讀,在另一個(gè)事務(wù)插入數(shù)據(jù)前未加間隙鎖,會(huì)發(fā)生什么呢?
接下來,我們稍作修改上面的SQL代碼,采用當(dāng)前讀方式來查詢數(shù)據(jù):
事務(wù)1 | 事務(wù)2 |
SET session TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
BEGIN; | |
SELECT * FROM users WHERE AGE > 10 AND AGE <30; | |
BEGIN; | |
INSERT INTO users(gmt_create, age, name) values(now(), 20, 'Paidaxing999'); | |
COMMIT; | |
SELECT * FROM users WHERE AGE > 10 AND AGE < 30; | |
SELECT * FROM users WHERE AGE > 10 AND AGE < 30 for update; |
在上面的例子中,在事務(wù)1中,我們并未在事務(wù)剛啟動(dòng)時(shí)立即加鎖,而是進(jìn)行了一次普通的查詢,隨后事務(wù)2成功插入數(shù)據(jù)后,事務(wù)1再進(jìn)行了兩次查詢。
我們觀察到,事務(wù)1后兩次查詢的結(jié)果完全不同。在沒有加鎖的情況下,即快照讀時(shí),讀取的數(shù)據(jù)與第一次查詢結(jié)果相同,從而避免了幻讀現(xiàn)象。但第二次查詢執(zhí)行了鎖定操作,即當(dāng)前讀,因此讀取到的數(shù)據(jù)中包含了其他事務(wù)提交的數(shù)據(jù),導(dǎo)致了幻讀的發(fā)生。
倘若您理解了上述例子以及當(dāng)前讀的概念,您將很容易意識(shí)到,下面的這個(gè)案例事實(shí)上也會(huì)導(dǎo)致幻讀的發(fā)生:
事務(wù)1 | 事務(wù)2 |
SET session TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
BEGIN; | |
SELECT * FROM users WHERE AGE > 10 AND AGE <30; | |
BEGIN; | |
INSERT INTO users(gmt_create, age, name) values(now(), 20, 'Paidaxing999'); | |
COMMIT; | |
SELECT * FROM users WHERE AGE > 10 AND AGE <30; | |
UPDATE users set name = "Paidaxing888" where age = 20; | |
SELECT * FROM users WHERE AGE > 10 AND AGE <30; |
這里產(chǎn)生幻讀的原因和前面的例子實(shí)際上是相同的。即,MVCC只能解決快照讀中的幻讀問題,而對(duì)于當(dāng)前讀(例如 SELECT FOR UPDATE、UPDATE、DELETE 等操作)仍會(huì)導(dǎo)致幻讀的產(chǎn)生。在同一個(gè)事務(wù)中同時(shí)進(jìn)行快照讀和當(dāng)前讀操作時(shí),將導(dǎo)致幻讀的發(fā)生。
UPDATE 語句也屬于當(dāng)前讀操作,因此它有可能讀取到其他事務(wù)提交的結(jié)果。
為何事務(wù)1最后一次查詢和倒數(shù)第二次查詢的結(jié)果會(huì)不同呢?
原因在于根據(jù)快照讀的定義,在可重復(fù)讀級(jí)別下,如果在本事務(wù)中發(fā)生了數(shù)據(jù)修改,將會(huì)更新快照數(shù)據(jù),因此最后一次查詢的結(jié)果也會(huì)相應(yīng)地發(fā)生變化。
如何避免幻讀
了解了幻讀產(chǎn)生的情境以及無法解決的幾種情況后,讓我們總結(jié)一下如何解決幻讀的問題。
首先,若欲徹底解決幻讀問題,在 InnoDB 中唯一可選的隔離級(jí)別是 Serializable(可串行化)級(jí)別。
圖源:MySQL 8.0 參考手冊(cè)
若希望在一定程度上解決或避免幻讀,可考慮使用可重復(fù)讀(RR)隔離級(jí)別,但讀提交(RC)和讀未提交(RU)級(jí)別肯定不可行。
在可重復(fù)讀級(jí)別中,盡量使用快照讀(無鎖查詢),這樣不僅可以減少鎖沖突、提高并發(fā)度,還能避免幻讀問題的發(fā)生。
在高并發(fā)場(chǎng)景中若必須加鎖,應(yīng)在事務(wù)開始時(shí)立即加鎖,這將引入間隙鎖,有效地避免幻讀。
然而,值得注意的是,間隙鎖是引發(fā)死鎖的重要因素,因此在使用時(shí)需要謹(jǐn)慎對(duì)待。