Mysql中幻讀的概念以及如何解決
準(zhǔn)備工作
為了便于說(shuō)明問(wèn)題,這一篇文章,我們就先使用一個(gè)小一點(diǎn)兒的表。
CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`) ) ENGINE=InnoDB; ? insert into t values(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25);
這個(gè)表除了主鍵id外,還有一個(gè)索引c,初始化語(yǔ)句在表中插入了6行數(shù)據(jù)。
下面的語(yǔ)句序列,是怎么加鎖的,加的鎖又是什么時(shí)候釋放的呢?
begin; select * from t where d=5 for update; commit;
比較好理解的是,這個(gè)語(yǔ)句會(huì)命中d=5的這一行,對(duì)應(yīng)的主鍵id=5,因此在select 語(yǔ)句執(zhí)行完成后,id=5這一行會(huì)加一個(gè)寫鎖,而且由于兩階段鎖協(xié)議,這個(gè)寫鎖會(huì)在執(zhí)行commit語(yǔ)句的時(shí)候釋放。
由于字段d上沒(méi)有索引,因此這條查詢語(yǔ)句會(huì)做全表掃描。那么,其他被掃描到的,但是不滿足條件的5行記錄上,會(huì)不會(huì)被加鎖呢?
我們知道,InnoDB的默認(rèn)事務(wù)隔離級(jí)別是可重復(fù)讀,所以本文接下來(lái)沒(méi)有特殊說(shuō)明的部分,都是設(shè)定在可重復(fù)讀隔離級(jí)別下。
幻讀是什么?
現(xiàn)在,我們就來(lái)分析一下,如果只在id=5這一行加鎖,而其他行的不加鎖的話,會(huì)怎么樣。
下面先來(lái)看一下這個(gè)場(chǎng)景(注意:這是我假設(shè)的一個(gè)場(chǎng)景):

可以看到,session A里執(zhí)行了三次查詢,分別是Q1、Q2和Q3。它們的SQL語(yǔ)句相同,都是select * from t where d=5 for update。這個(gè)語(yǔ)句的意思你應(yīng)該很清楚了,查所有d=5的行,而且使用的是當(dāng)前讀,并且加上寫鎖?,F(xiàn)在,我們來(lái)看一下這三條SQL語(yǔ)句,分別會(huì)返回什么結(jié)果。
- Q1只返回id=5這一行;
- 在T2時(shí)刻,session B把id=0這一行的d值改成了5,因此T3時(shí)刻Q2查出來(lái)的是id=0和id=5這兩行;
- 在T4時(shí)刻,session C又插入一行(1,1,5),因此T5時(shí)刻Q3查出來(lái)的是id=0、id=1和id=5的這三行。
其中,Q3讀到id=1這一行的現(xiàn)象,被稱為“幻讀”。也就是說(shuō),幻讀指的是一個(gè)事務(wù)在前后兩次查詢同一個(gè)范圍的時(shí)候,后一次查詢看到了前一次查詢沒(méi)有看到的行。
這里,我需要對(duì)“幻讀”做一個(gè)說(shuō)明:
- 在可重復(fù)讀隔離級(jí)別下,普通的查詢是快照讀,是不會(huì)看到別的事務(wù)插入的數(shù)據(jù)的。因此,幻讀在“當(dāng)前讀”下才會(huì)出現(xiàn)。
- 上面session B的修改結(jié)果,被session A之后的select語(yǔ)句用“當(dāng)前讀”看到,不能稱為幻讀?;米x僅專指“新插入的行”。
因?yàn)檫@三個(gè)查詢都是加了for update,都是當(dāng)前讀。而當(dāng)前讀的規(guī)則,就是要能讀到所有已經(jīng)提交的記錄的最新值。并且,session B和sessionC的兩條語(yǔ)句,執(zhí)行后就會(huì)提交,所以Q2和Q3就是應(yīng)該看到這兩個(gè)事務(wù)的操作效果,而且也看到了,這跟事務(wù)的可見(jiàn)性規(guī)則并不矛盾。
幻讀有什么問(wèn)題?
首先是語(yǔ)義上的。session A在T1時(shí)刻就聲明了,“我要把所有d=5的行鎖住,不準(zhǔn)別的事務(wù)進(jìn)行讀寫操作”。而實(shí)際上,這個(gè)語(yǔ)義被破壞了。
如果現(xiàn)在這樣看感覺(jué)還不明顯的話,我再往session B和session C里面分別加一條SQL語(yǔ)句,你再看看會(huì)出現(xiàn)什么現(xiàn)象。

session B的第二條語(yǔ)句update t set c=5 where id=0,語(yǔ)義是“我把id=0、d=5這一行的c值,改成了5”。
由于在T1時(shí)刻,session A 還只是給id=5這一行加了行鎖, 并沒(méi)有給id=0這行加上鎖。因此,session B在T2時(shí)刻,是可以執(zhí)行這兩條update語(yǔ)句的。這樣,就破壞了 session A 里Q1語(yǔ)句要鎖住所有d=5的行的加鎖聲明。
session C也是一樣的道理,對(duì)id=1這一行的修改,也是破壞了Q1的加鎖聲明。
數(shù)據(jù)一致性問(wèn)題
我們知道,鎖的設(shè)計(jì)是為了保證數(shù)據(jù)的一致性。而這個(gè)一致性,不止是數(shù)據(jù)庫(kù)內(nèi)部數(shù)據(jù)狀態(tài)在此刻的一致性,還包含了數(shù)據(jù)和日志在邏輯上的一致性。
為了說(shuō)明這個(gè)問(wèn)題,我給session A在T1時(shí)刻再加一個(gè)更新語(yǔ)句,即:update t set d=100 where d=5。

update的加鎖語(yǔ)義和select …for update 是一致的,所以這時(shí)候加上這條update語(yǔ)句也很合理。session A聲明說(shuō)“要給d=5的語(yǔ)句加上鎖”,就是為了要更新數(shù)據(jù),新加的這條update語(yǔ)句就是把它認(rèn)為加上了鎖的這一行的d值修改成了100。
現(xiàn)在,我們來(lái)分析一下圖3執(zhí)行完成后,數(shù)據(jù)庫(kù)里會(huì)是什么結(jié)果。
- 經(jīng)過(guò)T1時(shí)刻,id=5這一行變成 (5,5,100),當(dāng)然這個(gè)結(jié)果最終是在T6時(shí)刻正式提交的;
- 經(jīng)過(guò)T2時(shí)刻,id=0這一行變成(0,5,5);
- 經(jīng)過(guò)T4時(shí)刻,表里面多了一行(1,5,5);
- 其他行跟這個(gè)執(zhí)行序列無(wú)關(guān),保持不變。
這樣看,這些數(shù)據(jù)也沒(méi)啥問(wèn)題,但是我們?cè)賮?lái)看看這時(shí)候binlog里面的內(nèi)容。
- T2時(shí)刻,session B事務(wù)提交,寫入了兩條語(yǔ)句;
- T4時(shí)刻,session C事務(wù)提交,寫入了兩條語(yǔ)句;
- T6時(shí)刻,session A事務(wù)提交,寫入了update t set d=100 where d=5 這條語(yǔ)句。
我統(tǒng)一放到一起的話,就是這樣的:
update t set d=5 where id=0; /*(0,0,5)*/ update t set c=5 where id=0; /*(0,5,5)*/ ? insert into t values(1,1,5); /*(1,1,5)*/ update t set c=5 where id=1; /*(1,5,5)*/ ? update t set d=100 where d=5;/*所有d=5的行,d改成100*/
好,你應(yīng)該看出問(wèn)題了。這個(gè)語(yǔ)句序列,不論是拿到備庫(kù)去執(zhí)行,還是以后用binlog來(lái)克隆一個(gè)庫(kù),這三行的結(jié)果,都變成了 (0,5,100)、(1,5,100)和(5,5,100)。
也就是說(shuō),id=0和id=1這兩行,發(fā)生了數(shù)據(jù)不一致。這個(gè)問(wèn)題很嚴(yán)重,是不行的。
到這里,我們?cè)倩仡櫼幌拢?strong>這個(gè)數(shù)據(jù)不一致到底是怎么引入的?
我們分析一下可以知道,這是我們假設(shè)“select * from t where d=5 for update這條語(yǔ)句只給d=5這一行,也就是id=5的這一行加鎖”導(dǎo)致的。
所以我們認(rèn)為,上面的設(shè)定不合理,要改。
那怎么改呢?我們把掃描過(guò)程中碰到的行,也都加上寫鎖,再來(lái)看看執(zhí)行效果。

由于session A把所有的行都加了寫鎖,所以session B在執(zhí)行第一個(gè)update語(yǔ)句的時(shí)候就被鎖住了。需要等到T6時(shí)刻session A提交以后,session B才能繼續(xù)執(zhí)行。
這樣對(duì)于id=0這一行,在數(shù)據(jù)庫(kù)里的最終結(jié)果還是 (0,5,5)。在binlog里面,執(zhí)行序列是這樣的:
insert into t values(1,1,5); /*(1,1,5)*/ update t set c=5 where id=1; /*(1,5,5)*/ ? update t set d=100 where d=5;/*所有d=5的行,d改成100*/ ? update t set d=5 where id=0; /*(0,0,5)*/ update t set c=5 where id=0; /*(0,5,5)*/
可以看到,按照日志順序執(zhí)行,id=0這一行的最終結(jié)果也是(0,5,5)。所以,id=0這一行的問(wèn)題解決了。
但同時(shí)你也可以看到,id=1這一行,在數(shù)據(jù)庫(kù)里面的結(jié)果是(1,5,5),而根據(jù)binlog的執(zhí)行結(jié)果是(1,5,100),也就是說(shuō)幻讀的問(wèn)題還是沒(méi)有解決。為什么我們已經(jīng)這么“兇殘”地,把所有的記錄都上了鎖,還是阻止不了id=1這一行的插入和更新呢?
原因很簡(jiǎn)單。在T3時(shí)刻,我們給所有行加鎖的時(shí)候,id=1這一行還不存在,不存在也就加不上鎖。
也就是說(shuō),即使把所有的記錄都加上鎖,還是阻止不了新插入的記錄,這也是為什么“幻讀”會(huì)被單獨(dú)拿出來(lái)解決的原因。
現(xiàn)在你知道了,產(chǎn)生幻讀的原因是,行鎖只能鎖住行,但是新插入記錄這個(gè)動(dòng)作,要更新的是記錄之間的“間隙”。因此,為了解決幻讀問(wèn)題,InnoDB只好引入新的鎖,也就是間隙鎖(Gap Lock)。
顧名思義,間隙鎖,鎖的就是兩個(gè)值之間的空隙。比如文章開(kāi)頭的表t,初始化插入了6個(gè)記錄,這就產(chǎn)生了7個(gè)間隙。

這樣,當(dāng)你執(zhí)行 select * from t where d=5 for update的時(shí)候,就不止是給數(shù)據(jù)庫(kù)中已有的6個(gè)記錄加上了行鎖,還同時(shí)加了7個(gè)間隙鎖。這樣就確保了無(wú)法再插入新的記錄。
也就是說(shuō)這時(shí)候,在一行行掃描的過(guò)程中,不僅將給行加上了行鎖,還給行兩邊的空隙,也加上了間隙鎖。
現(xiàn)在你知道了,數(shù)據(jù)行是可以加上鎖的實(shí)體,數(shù)據(jù)行之間的間隙,也是可以加上鎖的實(shí)體。但是間隙鎖跟我們之前碰到過(guò)的鎖都不太一樣。
比如行鎖,分成讀鎖和寫鎖。下圖就是這兩種類型行鎖的沖突關(guān)系。
也就是說(shuō),跟行鎖有沖突關(guān)系的是“另外一個(gè)行鎖”。
但是間隙鎖不一樣,跟間隙鎖存在沖突關(guān)系的,是“往這個(gè)間隙中插入一個(gè)記錄”這個(gè)操作。間隙鎖之間都不存在沖突關(guān)系。
這句話不太好理解,我給你舉個(gè)例子:

這里session B并不會(huì)被堵住。因?yàn)楸韙里并沒(méi)有c=7這個(gè)記錄,因此session A加的是間隙鎖(5,10)。而session B也是在這個(gè)間隙加的間隙鎖。它們有共同的目標(biāo),即:保護(hù)這個(gè)間隙,不允許插入值。但,它們之間是不沖突的。
間隙鎖和行鎖合稱next-key lock,每個(gè)next-key lock是前開(kāi)后閉區(qū)間。也就是說(shuō),我們的表t初始化以后,如果用select * from t for update要把整個(gè)表所有記錄鎖起來(lái),就形成了7個(gè)next-key lock,分別是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。
你可能會(huì)問(wèn)說(shuō),這個(gè)supremum從哪兒來(lái)的呢?
這是因?yàn)?∞是開(kāi)區(qū)間。實(shí)現(xiàn)上,InnoDB給每個(gè)索引加了一個(gè)不存在的最大值supremum,這樣才符合我們前面說(shuō)的“都是前開(kāi)后閉區(qū)間”。
間隙鎖和next-key lock的引入,幫我們解決了幻讀的問(wèn)題,但同時(shí)也帶來(lái)了一些“困擾”。
到此這篇關(guān)于Mysql中幻讀的概念以及如何解決的文章就介紹到這了,更多相關(guān)Mysql 幻讀概念內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mysql觸發(fā)器實(shí)時(shí)檢測(cè)一條語(yǔ)句進(jìn)行備份刪除思路詳解
遇到過(guò)這樣一個(gè)需求,在一張表里會(huì)不時(shí)出現(xiàn) “違規(guī)” 字樣的字段,需要在出現(xiàn)這個(gè)字段的時(shí)候,把整行的數(shù)據(jù)刪掉,針對(duì)這個(gè)需求我們?cè)撊绾尾僮髂兀旅娓S小編看下mysql觸發(fā)器實(shí)時(shí)檢測(cè)一條語(yǔ)句進(jìn)行備份刪除的解決思路,一起看看吧2021-09-09
Mysql開(kāi)啟外網(wǎng)訪問(wèn)的全過(guò)程記錄
mysql 默認(rèn)是不允許外放訪問(wèn)的,只允許 localhost 或 127.0.0.1 訪問(wèn),下面這篇文章主要給大家介紹了關(guān)于Mysql開(kāi)啟外網(wǎng)訪問(wèn)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-05-05
MySQL中實(shí)現(xiàn)高性能高并發(fā)計(jì)數(shù)器方案(例如文章點(diǎn)擊數(shù))
這篇文章主要介紹了MySQL中實(shí)現(xiàn)高性能高并發(fā)計(jì)數(shù)器方案,本文中的計(jì)數(shù)器是指如文章的點(diǎn)擊數(shù)、喜歡數(shù)、瀏覽次數(shù)等,需要的朋友可以參考下2014-10-10
ssm框架如何調(diào)用mysql存儲(chǔ)過(guò)程
這篇文章主要介紹了ssm框架如何調(diào)用mysql存儲(chǔ)過(guò)程,首先是建表,創(chuàng)建存儲(chǔ)過(guò)程,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-05-05
MySQL用B+樹作為索引結(jié)構(gòu)有什么好處
這篇文章主要介紹了MySQL用B+樹作為索引結(jié)構(gòu)有什么好處,幫助大家更好的理解和使用MySQL 索引,感興趣的朋友可以了解下2021-01-01
在idea中使用mysql數(shù)據(jù)庫(kù)超詳細(xì)教程
這篇文章主要介紹了如何在IntelliJ IDEA中連接MySQL數(shù)據(jù)庫(kù),并使用控制臺(tái)執(zhí)行SQL語(yǔ)句,還詳細(xì)講解了如何使用MyBatis Generator快速生成實(shí)體類、接口和SQL文件,以及如何解決Lombok依賴問(wèn)題,需要的朋友可以參考下2025-02-02

