Mysql中幻讀的概念以及如何解決
準(zhǔ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,初始化語句在表中插入了6行數(shù)據(jù)。
下面的語句序列,是怎么加鎖的,加的鎖又是什么時(shí)候釋放的呢?
begin; select * from t where d=5 for update; commit;
比較好理解的是,這個(gè)語句會(huì)命中d=5的這一行,對(duì)應(yīng)的主鍵id=5,因此在select 語句執(zhí)行完成后,id=5這一行會(huì)加一個(gè)寫鎖,而且由于兩階段鎖協(xié)議,這個(gè)寫鎖會(huì)在執(zhí)行commit語句的時(shí)候釋放。
由于字段d上沒有索引,因此這條查詢語句會(huì)做全表掃描。那么,其他被掃描到的,但是不滿足條件的5行記錄上,會(huì)不會(huì)被加鎖呢?
我們知道,InnoDB的默認(rèn)事務(wù)隔離級(jí)別是可重復(fù)讀,所以本文接下來沒有特殊說明的部分,都是設(shè)定在可重復(fù)讀隔離級(jí)別下。
幻讀是什么?
現(xiàn)在,我們就來分析一下,如果只在id=5這一行加鎖,而其他行的不加鎖的話,會(huì)怎么樣。
下面先來看一下這個(gè)場景(注意:這是我假設(shè)的一個(gè)場景):
可以看到,session A里執(zhí)行了三次查詢,分別是Q1、Q2和Q3。它們的SQL語句相同,都是select * from t where d=5 for update。這個(gè)語句的意思你應(yīng)該很清楚了,查所有d=5的行,而且使用的是當(dāng)前讀,并且加上寫鎖?,F(xiàn)在,我們來看一下這三條SQL語句,分別會(huì)返回什么結(jié)果。
- Q1只返回id=5這一行;
- 在T2時(shí)刻,session B把id=0這一行的d值改成了5,因此T3時(shí)刻Q2查出來的是id=0和id=5這兩行;
- 在T4時(shí)刻,session C又插入一行(1,1,5),因此T5時(shí)刻Q3查出來的是id=0、id=1和id=5的這三行。
其中,Q3讀到id=1這一行的現(xiàn)象,被稱為“幻讀”。也就是說,幻讀指的是一個(gè)事務(wù)在前后兩次查詢同一個(gè)范圍的時(shí)候,后一次查詢看到了前一次查詢沒有看到的行。
這里,我需要對(duì)“幻讀”做一個(gè)說明:
- 在可重復(fù)讀隔離級(jí)別下,普通的查詢是快照讀,是不會(huì)看到別的事務(wù)插入的數(shù)據(jù)的。因此,幻讀在“當(dāng)前讀”下才會(huì)出現(xiàn)。
- 上面session B的修改結(jié)果,被session A之后的select語句用“當(dāng)前讀”看到,不能稱為幻讀?;米x僅專指“新插入的行”。
因?yàn)檫@三個(gè)查詢都是加了for update,都是當(dāng)前讀。而當(dāng)前讀的規(guī)則,就是要能讀到所有已經(jīng)提交的記錄的最新值。并且,session B和sessionC的兩條語句,執(zhí)行后就會(huì)提交,所以Q2和Q3就是應(yīng)該看到這兩個(gè)事務(wù)的操作效果,而且也看到了,這跟事務(wù)的可見性規(guī)則并不矛盾。
幻讀有什么問題?
首先是語義上的。session A在T1時(shí)刻就聲明了,“我要把所有d=5的行鎖住,不準(zhǔn)別的事務(wù)進(jìn)行讀寫操作”。而實(shí)際上,這個(gè)語義被破壞了。
如果現(xiàn)在這樣看感覺還不明顯的話,我再往session B和session C里面分別加一條SQL語句,你再看看會(huì)出現(xiàn)什么現(xiàn)象。
session B的第二條語句update t set c=5 where id=0,語義是“我把id=0、d=5這一行的c值,改成了5”。
由于在T1時(shí)刻,session A 還只是給id=5這一行加了行鎖, 并沒有給id=0這行加上鎖。因此,session B在T2時(shí)刻,是可以執(zhí)行這兩條update語句的。這樣,就破壞了 session A 里Q1語句要鎖住所有d=5的行的加鎖聲明。
session C也是一樣的道理,對(duì)id=1這一行的修改,也是破壞了Q1的加鎖聲明。
數(shù)據(jù)一致性問題
我們知道,鎖的設(shè)計(jì)是為了保證數(shù)據(jù)的一致性。而這個(gè)一致性,不止是數(shù)據(jù)庫內(nèi)部數(shù)據(jù)狀態(tài)在此刻的一致性,還包含了數(shù)據(jù)和日志在邏輯上的一致性。
為了說明這個(gè)問題,我給session A在T1時(shí)刻再加一個(gè)更新語句,即:update t set d=100 where d=5。
update的加鎖語義和select …for update 是一致的,所以這時(shí)候加上這條update語句也很合理。session A聲明說“要給d=5的語句加上鎖”,就是為了要更新數(shù)據(jù),新加的這條update語句就是把它認(rèn)為加上了鎖的這一行的d值修改成了100。
現(xiàn)在,我們來分析一下圖3執(zhí)行完成后,數(shù)據(jù)庫里會(huì)是什么結(jié)果。
- 經(jīng)過T1時(shí)刻,id=5這一行變成 (5,5,100),當(dāng)然這個(gè)結(jié)果最終是在T6時(shí)刻正式提交的;
- 經(jīng)過T2時(shí)刻,id=0這一行變成(0,5,5);
- 經(jīng)過T4時(shí)刻,表里面多了一行(1,5,5);
- 其他行跟這個(gè)執(zhí)行序列無關(guān),保持不變。
這樣看,這些數(shù)據(jù)也沒啥問題,但是我們再來看看這時(shí)候binlog里面的內(nèi)容。
- T2時(shí)刻,session B事務(wù)提交,寫入了兩條語句;
- T4時(shí)刻,session C事務(wù)提交,寫入了兩條語句;
- T6時(shí)刻,session A事務(wù)提交,寫入了update t set d=100 where d=5 這條語句。
我統(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)該看出問題了。這個(gè)語句序列,不論是拿到備庫去執(zhí)行,還是以后用binlog來克隆一個(gè)庫,這三行的結(jié)果,都變成了 (0,5,100)、(1,5,100)和(5,5,100)。
也就是說,id=0和id=1這兩行,發(fā)生了數(shù)據(jù)不一致。這個(gè)問題很嚴(yán)重,是不行的。
到這里,我們再回顧一下,這個(gè)數(shù)據(jù)不一致到底是怎么引入的?
我們分析一下可以知道,這是我們假設(shè)“select * from t where d=5 for update這條語句只給d=5這一行,也就是id=5的這一行加鎖”導(dǎo)致的。
所以我們認(rèn)為,上面的設(shè)定不合理,要改。
那怎么改呢?我們把掃描過程中碰到的行,也都加上寫鎖,再來看看執(zhí)行效果。
由于session A把所有的行都加了寫鎖,所以session B在執(zhí)行第一個(gè)update語句的時(shí)候就被鎖住了。需要等到T6時(shí)刻session A提交以后,session B才能繼續(xù)執(zhí)行。
這樣對(duì)于id=0這一行,在數(shù)據(jù)庫里的最終結(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這一行的問題解決了。
但同時(shí)你也可以看到,id=1這一行,在數(shù)據(jù)庫里面的結(jié)果是(1,5,5),而根據(jù)binlog的執(zhí)行結(jié)果是(1,5,100),也就是說幻讀的問題還是沒有解決。為什么我們已經(jīng)這么“兇殘”地,把所有的記錄都上了鎖,還是阻止不了id=1這一行的插入和更新呢?
原因很簡單。在T3時(shí)刻,我們給所有行加鎖的時(shí)候,id=1這一行還不存在,不存在也就加不上鎖。
也就是說,即使把所有的記錄都加上鎖,還是阻止不了新插入的記錄,這也是為什么“幻讀”會(huì)被單獨(dú)拿出來解決的原因。
現(xiàn)在你知道了,產(chǎn)生幻讀的原因是,行鎖只能鎖住行,但是新插入記錄這個(gè)動(dòng)作,要更新的是記錄之間的“間隙”。因此,為了解決幻讀問題,InnoDB只好引入新的鎖,也就是間隙鎖(Gap Lock)。
顧名思義,間隙鎖,鎖的就是兩個(gè)值之間的空隙。比如文章開頭的表t,初始化插入了6個(gè)記錄,這就產(chǎn)生了7個(gè)間隙。
這樣,當(dāng)你執(zhí)行 select * from t where d=5 for update的時(shí)候,就不止是給數(shù)據(jù)庫中已有的6個(gè)記錄加上了行鎖,還同時(shí)加了7個(gè)間隙鎖。這樣就確保了無法再插入新的記錄。
也就是說這時(shí)候,在一行行掃描的過程中,不僅將給行加上了行鎖,還給行兩邊的空隙,也加上了間隙鎖。
現(xiàn)在你知道了,數(shù)據(jù)行是可以加上鎖的實(shí)體,數(shù)據(jù)行之間的間隙,也是可以加上鎖的實(shí)體。但是間隙鎖跟我們之前碰到過的鎖都不太一樣。
比如行鎖,分成讀鎖和寫鎖。下圖就是這兩種類型行鎖的沖突關(guān)系。
也就是說,跟行鎖有沖突關(guān)系的是“另外一個(gè)行鎖”。
但是間隙鎖不一樣,跟間隙鎖存在沖突關(guān)系的,是“往這個(gè)間隙中插入一個(gè)記錄”這個(gè)操作。間隙鎖之間都不存在沖突關(guān)系。
這句話不太好理解,我給你舉個(gè)例子:
這里session B并不會(huì)被堵住。因?yàn)楸韙里并沒有c=7這個(gè)記錄,因此session A加的是間隙鎖(5,10)。而session B也是在這個(gè)間隙加的間隙鎖。它們有共同的目標(biāo),即:保護(hù)這個(gè)間隙,不允許插入值。但,它們之間是不沖突的。
間隙鎖和行鎖合稱next-key lock,每個(gè)next-key lock是前開后閉區(qū)間。也就是說,我們的表t初始化以后,如果用select * from t for update要把整個(gè)表所有記錄鎖起來,就形成了7個(gè)next-key lock,分別是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。
你可能會(huì)問說,這個(gè)supremum從哪兒來的呢?
這是因?yàn)?∞是開區(qū)間。實(shí)現(xiàn)上,InnoDB給每個(gè)索引加了一個(gè)不存在的最大值supremum,這樣才符合我們前面說的“都是前開后閉區(qū)間”。
間隙鎖和next-key lock的引入,幫我們解決了幻讀的問題,但同時(shí)也帶來了一些“困擾”。
到此這篇關(guān)于Mysql中幻讀的概念以及如何解決的文章就介紹到這了,更多相關(guān)Mysql 幻讀概念內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MySQL數(shù)據(jù)庫的觸發(fā)器和事務(wù)
這篇文章主要介紹了MySQL數(shù)據(jù)庫的觸發(fā)器和事務(wù),觸發(fā)器是SQL?server提供給程序員和數(shù)據(jù)分析員來保證數(shù)據(jù)完整性的一種方法,它是與表事件相關(guān)的特殊的存儲(chǔ)過程,是由事件來觸發(fā)2022-08-08安裝配置mysql及Navicat prenium的詳細(xì)流程
這篇文章主要介紹了安裝配置mysql及Navicat Premium的詳細(xì)流程,配置方法也真的很簡單,本文給大家詳細(xì)介紹mysql Navicat Premium安裝配置相關(guān)知識(shí)感興趣的朋友,一起學(xué)習(xí)吧2021-06-06IDEA連接mysql又報(bào)錯(cuò)!Server returns invalid timezone. Go to tab an
這篇文章主要介紹了IDEA連接mysql又報(bào)錯(cuò)!Server returns invalid timezone. Go to 'Advanced' tab and set 'serverTimezone' prope問題,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),需要的朋友可以參考下2020-05-05