MySQL到底在RR層面解決幻讀了嗎?

Y說

hi,好久不見。

最近工作上有一些變動(dòng),文章很少更新。不過平時(shí)還是有收集一些文章idea,后面有空會(huì)慢慢寫~

這篇文章是因?yàn)橹皩懥艘黄P(guān)于InnoDB鎖的文章,在個(gè)人網(wǎng)站上有讀者留言問“間隙鎖一定程度上解決了幻讀問題,為什么不是完全解決了呢”,所以重新把MySQL中關(guān)于幻讀的問題梳理了一遍。

文章寫完后,發(fā)現(xiàn)自己也有新的收獲,又了解了一些奇怪的知識(shí)點(diǎn),哈哈。

隔離級(jí)別與幻讀

數(shù)據(jù)庫(kù)的事務(wù)有四種隔離級(jí)別,這四種隔離級(jí)別分別應(yīng)用在不同的場(chǎng)景,隔離級(jí)別越低,并發(fā)性越高,但數(shù)據(jù)一致性就越差。四種隔離級(jí)別存在的問題如下表:

隔離級(jí)別 臟讀 不可重復(fù)讀 幻讀
Read uncommitted
Read Committed
Repeatable Reads
Serializable

MySQL InnoDB默認(rèn)的隔離級(jí)別是Repeatable Reads(RR),但它通過MVCC+間隙鎖解決了絕大部分幻讀(后面會(huì)解釋為什么是絕大部分而不是全部)的問題。

什么是幻讀

簡(jiǎn)單來說,一個(gè)事務(wù)同樣的查詢條件,兩次查詢到的數(shù)據(jù)行數(shù)不一樣,它就產(chǎn)生了幻讀。

舉例來說,對(duì)于下面的數(shù)據(jù):

id c d
5 5 5
10 10 10
15 15 15

事務(wù)A第一次查詢sql:

SELECT * FROM demo WHERE id < 10;

這個(gè)時(shí)候只能查出 id = 5 的數(shù)據(jù)。

而這個(gè)時(shí)候如果事務(wù)B插入了一條id = 6的數(shù)據(jù):

INSERT INTO demo VALUES (7, 7, 7);

然后A進(jìn)行第二次查詢,用同樣的sql,如果查到了兩條數(shù)據(jù):id = 5 和 id = 7,那就是出現(xiàn)了幻讀。對(duì)于事務(wù)A來說,就像幻覺一樣,同樣的查詢條件,查出來的數(shù)據(jù)多了一行。

這個(gè)在業(yè)務(wù)場(chǎng)景中也很常見:先查詢名為“編了個(gè)程”的公眾號(hào),發(fā)現(xiàn)不存在,于是我準(zhǔn)備創(chuàng)建的時(shí)候,卻發(fā)現(xiàn)已經(jīng)被其它事務(wù)創(chuàng)建了,于是唯一索引提示我創(chuàng)建失敗,改名稱已存在。那我這個(gè)事務(wù)豈不是出現(xiàn)幻覺了?明明剛剛查過不存在的呀~

快照讀和當(dāng)前讀

快照讀

在MySQL中,查詢語句分為兩種,一種是簡(jiǎn)單的select操作,屬于快照讀,不加鎖。它讀的是記錄的快照版本(這個(gè)版本跟MVCC有關(guān),后面再細(xì)聊)。

select * from table where ?;

當(dāng)前讀

另一種是要加鎖的特殊的讀操作,它讀的是記錄的最新版本。

-- 共享讀鎖
select * from table where ? lock in share mode;
-- 共享寫鎖
select * from table where ? for update;

-- 增刪改也屬于當(dāng)前讀,因?yàn)橐瓤催@條記錄在不在
insert into table values (…);
update table set ? where ?;
delete from table where ?;

快照讀下的幻讀

InnoDB是通過MVCC來解決當(dāng)前讀下的幻讀問題的。每個(gè)事務(wù)會(huì)有一個(gè)遞增的事務(wù)ID,每行記錄都有兩個(gè)隱藏字段:創(chuàng)建版本和刪除版本。在進(jìn)行操作時(shí),遵循以下規(guī)則:

INSERT

保存當(dāng)前事務(wù)id作為行版本號(hào)

DELETE

保存當(dāng)前的事務(wù)id到這行數(shù)據(jù)的“刪除版本”。

UPDATE

插入一行新記錄,保存當(dāng)前事務(wù)id作為行版本號(hào),同時(shí)保存當(dāng)前事務(wù)id到原來的行的“刪除版本”。

SELECT

  • 只讀取版本小于等于當(dāng)前事務(wù)id的行。這樣可以保證事務(wù)讀取都的行,要么之前就存在,要么是這個(gè)事務(wù)本身自己插入或者修改的。
  • 只讀取“刪除版本”為空或者小于等于當(dāng)前事務(wù)id的行。這樣可以確保事務(wù)讀取到的行,在事務(wù)之前沒有被刪除。

當(dāng)前讀下的幻讀

當(dāng)前讀下的幻讀是通過間隙鎖(gap_lock)來實(shí)現(xiàn)的。在事務(wù)A查詢的時(shí)候,會(huì)鎖住一個(gè)間隙,其它事務(wù)往這個(gè)間隙插入、刪除等操作都是會(huì)被鎖阻塞的。

關(guān)于間隙鎖是如何工作的,可以參考我之前寫的這篇文章:《InnoDB的行鎖,原來為你做了這么多!》,簡(jiǎn)單來說,間隙鎖和插入意向鎖互斥,徹底解決了當(dāng)前讀下的幻讀問題。

快照讀沒有完全解決幻讀?

是的,MySQL沒有完全解決快照讀下的幻讀問題。

可以做這個(gè)實(shí)驗(yàn):

  1. 當(dāng)前DB已有id 5, 10, 15三條數(shù)據(jù)。

  2. 事務(wù)A查詢id < 10的數(shù)據(jù),可以查出一行記錄id = 5

  3. 事務(wù)B插入id = 6的數(shù)據(jù)

  4. 事務(wù)A再查詢id < 10的數(shù)據(jù),可以查出一行記錄id = 5,查不出id = 6的數(shù)據(jù)(讀場(chǎng)景,解決了幻讀)

  5. 事務(wù)A可以更新/刪除id = 6的數(shù)據(jù),不能插入id = 6的數(shù)據(jù)(寫場(chǎng)景,幻讀不徹底)

這個(gè)很好理解,MySQL雖然通過MVCC的版本號(hào)來解決了讀場(chǎng)景下的幻讀,但對(duì)于上面第5步那種寫場(chǎng)景的情況,其實(shí)是無能為力的,因?yàn)镸VCC畢竟是無鎖實(shí)現(xiàn)。

所以如果后續(xù)要對(duì)數(shù)據(jù)進(jìn)行寫操作,還是通過for update語句上鎖比較穩(wěn)妥,不然就可能會(huì)出現(xiàn)上面第5步那樣的問題。

求個(gè)支持

我是Yasin,一個(gè)堅(jiān)持技術(shù)原創(chuàng)的博主,我的微信公眾號(hào)是:編了個(gè)程

都看到這兒了,如果覺得我的文章寫得還行,不妨支持一下。

文章會(huì)首發(fā)到公眾號(hào),閱讀體驗(yàn)最佳,歡迎大家關(guān)注。

你的每一個(gè)轉(zhuǎn)發(fā)、關(guān)注、點(diǎn)贊、評(píng)論都是對(duì)我最大的支持!

還有學(xué)習(xí)資源、和一線互聯(lián)網(wǎng)公司內(nèi)推哦

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容