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):
當(dāng)前DB已有id 5, 10, 15三條數(shù)據(jù)。
事務(wù)A查詢id < 10的數(shù)據(jù),可以查出一行記錄id = 5
事務(wù)B插入id = 6的數(shù)據(jù)
事務(wù)A再查詢id < 10的數(shù)據(jù),可以查出一行記錄id = 5,查不出id = 6的數(shù)據(jù)(讀場(chǎng)景,解決了幻讀)
事務(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)推哦