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