本文討論大部分基于下表進(jìn)行:
開始討論之前舒裤,先讓我們了解一些基本概念淘这。
一掉伏、并發(fā)事務(wù)帶來的問題
在典型的應(yīng)用程序中,經(jīng)常會產(chǎn)生多個事務(wù)并發(fā)運行两曼。雖然并發(fā)事務(wù)能夠極大的提升系統(tǒng)的可用性皂甘,但是也帶來了如下問題:
臟讀(Dirty Read)
事務(wù)T1修改了一行數(shù)據(jù)Row,事務(wù)T2在T1未提交前讀到了該行數(shù)據(jù)(Row)并使用該數(shù)據(jù)進(jìn)行業(yè)務(wù)操作悼凑。
丟失修改(Lost To Modify)
事務(wù)T1讀取了一行數(shù)據(jù)Row偿枕,事務(wù)T2在隨后也訪問了該行數(shù)據(jù)璧瞬,隨后事務(wù)T1對Row進(jìn)行了更新,事務(wù)T2也隨后對Row進(jìn)行更新渐夸,事務(wù)T2對Row的更新導(dǎo)致了事務(wù)T1對Row的更新丟失嗤锉。
不可重復(fù)讀(Non-repeatable Read)
事務(wù)T1讀取了一行數(shù)據(jù)Row,事務(wù)T2在T1讀取Row后對Row數(shù)據(jù)進(jìn)行了更新墓塌,T1再次讀取行Row瘟忱,發(fā)現(xiàn)與第一次讀取數(shù)據(jù)不一致,即在同一事務(wù)內(nèi)兩次執(zhí)行同一查詢得到的結(jié)果不一致苫幢。
幻讀(Phantom Read)
事務(wù)T1讀取了一個結(jié)果集ResultSet1访诱,事務(wù)T2在T1讀取ResultSet1后新增了一行數(shù)據(jù)Row,剛好這個事務(wù)T2新插入的這條數(shù)據(jù)滿足T1的抓取規(guī)則韩肝,導(dǎo)致事務(wù)T1再次讀取數(shù)據(jù)時触菜,得到了比ResultSet1更多的結(jié)果集ResultSet2。
關(guān)于不可重復(fù)讀和幻讀的區(qū)別
從上述定義看不可重復(fù)讀和幻讀伞梯,都是在一個事務(wù)中多次讀取得到了不同的數(shù)據(jù)結(jié)果玫氢。不可重復(fù)讀的重點在于修改,而幻讀的重點在于新增或刪除谜诫。
二漾峡、SQL隔離級別
ANSI SQL STANDARD定義了4類隔離級別(READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE),不同版本的數(shù)據(jù)庫分別實現(xiàn)了上述隔離級別的一個或者多個喻旷,這些不同的事務(wù)隔離級別用來限定事務(wù)內(nèi)外的哪些改變是可見的(指對其他事務(wù))生逸。一般說來,低級別的隔離級別一般支持更高的并發(fā)處理且预,并擁有更低的系統(tǒng)開銷送丰。ANSI SQL STANDARD定義的4類事務(wù)隔離級別如下:
Read Uncommitted(讀未提交)
允許讀取其他事務(wù)未提交的數(shù)據(jù)瘪菌,在該隔離級別下名党,所有事務(wù)都可以看到其他未提交事務(wù)的執(zhí)行結(jié)果帝际。由于本隔離級別很容易讀取到其他事務(wù)未提交的臟數(shù)據(jù),所以一般很少用于實際應(yīng)用涮拗。
Read Committed(讀已提交)
只能讀取其他事務(wù)已提交的更改乾戏。Oracle、SqlServer等大部分?jǐn)?shù)據(jù)庫的默認(rèn)隔離級別三热。
Repeatable Read(可重讀)
同一事務(wù)多次讀取數(shù)據(jù)時鼓择,會看到同樣的數(shù)據(jù)行。這是Mysql默認(rèn)的隔離級別就漾。ANSI SQL STANDARD對于可重讀隔離級別呐能,是允許出現(xiàn)幻讀的,而InnoDB存儲引擎通過多版本并發(fā)控制(MVCC抑堡,Multiversion Concurrency Control)機(jī)制解決了該問題摆出。但是對于InnoDB在RR隔離級別下是否完全解決幻讀朗徊,尚有爭議,我們將在下文進(jìn)行討論懊蒸。
Serializable(可串行化)
強(qiáng)制事務(wù)排序荣倾,事務(wù)串行化執(zhí)行。讀數(shù)據(jù)會加上共享鎖骑丸,讀寫會相互阻塞,在這個級別下妒貌,可能會產(chǎn)生大量的超時現(xiàn)象和鎖競爭通危。
下圖是ANSI SQL STANDARD對于各種隔離級別下允許出現(xiàn)的并發(fā)事務(wù)問題的規(guī)定:
三、InnoDB MVCC理解
ANSI SQL STANDARD定義了4類隔離級別灌曙,隨著隔離級別的提升菊碟,并發(fā)事務(wù)產(chǎn)生數(shù)據(jù)不一致性的問題就會大大越低,但是并發(fā)處理能力也會大大降低在刺,而不同的隔離級別逆害,往往都是通過鎖機(jī)制來解決并發(fā)事務(wù)產(chǎn)生的各種問題。數(shù)據(jù)的鎖定分為兩種方法蚣驼,一種叫悲觀鎖魄幕,另外一種叫做樂觀鎖。絕大部分商業(yè)數(shù)據(jù)庫(MySQL颖杏、Oracle等)為了性能考慮纯陨,都是使用了以樂觀鎖為理論基礎(chǔ)的MVCC(Multi-Version Concurrency Control 多版本并發(fā)控制)來解決并發(fā)事務(wù)帶來的數(shù)據(jù)訪問問題。
悲觀鎖
數(shù)據(jù)對外界(包括本系統(tǒng)當(dāng)前的其他事務(wù)留储,以及來自外部系統(tǒng)的事務(wù)處理)修改持保守態(tài)度翼抠,在整個的數(shù)據(jù)處理過程中,將數(shù)據(jù)處于鎖定狀態(tài)获讳。悲觀鎖的實現(xiàn)阴颖,往往依靠數(shù)據(jù)庫提供的鎖機(jī)制(也只有數(shù)據(jù)庫層提供的鎖機(jī)制才能真正保證數(shù)據(jù)訪問的排他性,否則丐膝,即使在本系統(tǒng)中實現(xiàn)了加鎖機(jī)制量愧,也無法保證外部系統(tǒng)不會修改數(shù)據(jù))。在悲觀鎖的情況下尤误,為了保證事務(wù)的隔離性侠畔,就需要一致性鎖定讀。讀取數(shù)據(jù)時給加鎖损晤,其它事務(wù)無法修改這些數(shù)據(jù)软棺。修改刪除數(shù)據(jù)時也要加鎖,其它事務(wù)無法讀取這些數(shù)據(jù)尤勋。悲觀鎖大多數(shù)情況下依靠數(shù)據(jù)庫的鎖機(jī)制實現(xiàn)喘落,以保證操作最大程度的獨占性茵宪。但隨之而來的就是數(shù)據(jù)庫性能的大量開銷(特別是長事務(wù))。
樂觀鎖
大多是基于數(shù)據(jù)版本( Version )記錄機(jī)制實現(xiàn)瘦棋。何謂數(shù)據(jù)版本稀火?即為數(shù)據(jù)增加一個版本標(biāo)識,在基于數(shù)據(jù)庫表的版本解決方案中赌朋,一般是通過為數(shù)據(jù)庫表增加一個 “version” 字段來實現(xiàn)凰狞。讀取出數(shù)據(jù)時,將此版本號一同讀出沛慢,之后更新時赡若,對此版本號加一。此時团甲,將提交數(shù)據(jù)的版本數(shù)據(jù)與數(shù)據(jù)庫表對應(yīng)記錄的當(dāng)前版本信息進(jìn)行比對逾冬,如果提交的數(shù)據(jù)版本號大于數(shù)據(jù)庫表當(dāng)前版本號,則予以更新躺苦,否則認(rèn)為是過期數(shù)據(jù)身腻。MVCC的實現(xiàn)沒有固定的規(guī)范,每個數(shù)據(jù)庫都會有不同的實現(xiàn)方式匹厘。
MVCC在MySQL的InnoDB實現(xiàn)
在InnoDB中嘀趟,會在每行數(shù)據(jù)后添加兩個額外的隱藏的值來實現(xiàn)MVCC,這兩個值一個記錄這行數(shù)據(jù)何時被創(chuàng)建集乔,另外一個記錄這行數(shù)據(jù)何時過期(或者被刪除)去件。 在實際操作中,存儲的并不是時間扰路,而是事務(wù)的版本號尤溜,每開啟一個新事務(wù),事務(wù)的版本號就會遞增汗唱。 在可重讀Repeatable Reads事務(wù)隔離級別下:
SELECT時宫莱,讀取創(chuàng)建版本號<=當(dāng)前事務(wù)版本號,刪除版本號為空或>當(dāng)前事務(wù)版本號哩罪。
INSERT時授霸,保存當(dāng)前事務(wù)版本號為行的創(chuàng)建版本號
DELETE時,保存當(dāng)前事務(wù)版本號為行的刪除版本號
UPDATE時际插,插入一條新紀(jì)錄碘耳,保存當(dāng)前事務(wù)版本號為行創(chuàng)建版本號,同時保存當(dāng)前事務(wù)版本號到原來刪除的行
通過MVCC框弛,雖然每行記錄都需要額外的存儲空間辛辨,更多的行檢查工作以及一些額外的維護(hù)工作,但可以減少鎖的使用,大多數(shù)讀操作都不用加鎖斗搞,讀數(shù)據(jù)操作很簡單指攒,性能很好,并且也能保證只會讀取到符合標(biāo)準(zhǔn)的行僻焚,也只鎖住必要行允悦。
四、InnoDB RR防止幻讀初探
Repeatable Read是nnoDB的默認(rèn)隔離級別虑啤,意味著在RR隔離級別下隙弛,一個事務(wù)在多個實例并發(fā)讀取數(shù)據(jù)時,會看到同樣的數(shù)據(jù)行狞山。關(guān)于RC(讀已提交)和RR(可重讀)的區(qū)別驶鹉,我們用如下兩圖進(jìn)行一個說明:
可以看到,在RC隔離級別下铣墨,事務(wù)A前后兩次執(zhí)行同一查詢語句得到了不同的結(jié)果,這就很可能帶來一些問題办绝。下面我們看看在RR隔離級別下的查詢結(jié)果伊约,如下圖所示:
從上圖可以看到,在RR隔離級別下孕蝉,事務(wù)A前后兩次執(zhí)行同一查詢語句得到的結(jié)果相同屡律,參閱并發(fā)事務(wù)帶來的問題,我們發(fā)現(xiàn)降淮,InnoDB在RR隔離級別下超埋,能夠解決并發(fā)事務(wù)帶來的臟讀、不可重復(fù)讀佳鳖、幻讀霍殴,因此很多人說InnoDB在RR級別下能夠防止幻讀(后文將展示出現(xiàn)幻讀的情況)。我們從MVCC的角度系吩,再來看看上述查詢:
五来庭、InnoDB RR無法防止幻讀初探
從上一小節(jié)中看到,InnoDB在RR隔離級別下穿挨,確實未讀取到其他并發(fā)事務(wù)的數(shù)據(jù)寫入和更新月弛,我們可能會認(rèn)為InnoDB的RR確實防止住了幻讀,但經(jīng)過測試科盛,發(fā)現(xiàn)了如下問題:
可以看到帽衙,session1在執(zhí)行一次update操作后,讀取到了session2中insert的數(shù)據(jù)贞绵,發(fā)生了幻讀厉萝,因此有人說InnoDB在RR隔離級別下無法防止幻讀。但是從ANSI SQL STANDARD定義的4類隔離級別可以看到,在RR隔離級別下冀泻,是允許出現(xiàn)幻讀的常侣,所以InnoDB在RR隔離級別下出現(xiàn)幻讀并不屬于BUG。
六弹渔、InnoDB在RR隔離級別下的當(dāng)前讀胳施、快照讀、行鎖肢专、Next-Key鎖
從上文內(nèi)容可以看到舞肆,InnoDB在RR隔離級別時,在某些情況下能夠防止幻讀博杖,但是在某些情況下會出現(xiàn)幻讀椿胯,為什么會出現(xiàn)這種情況?先讓我們了解如下幾個概念:
快照讀和當(dāng)前讀
事務(wù)的隔離級別其實都是對于讀數(shù)據(jù)的定義剃根,但是MySQL中的讀哩盲,和事務(wù)隔離級別中的讀,是不一樣的狈醉。在RR級別中廉油,通過MVCC機(jī)制,雖然讓數(shù)據(jù)變得可重復(fù)讀苗傅,但我們讀到的數(shù)據(jù)可能是歷史數(shù)據(jù)抒线,是不及時的數(shù)據(jù),不是數(shù)據(jù)庫當(dāng)前的數(shù)據(jù)渣慕!這在一些對于數(shù)據(jù)的時效特別敏感的業(yè)務(wù)中嘶炭,就很可能出問題。對于這種讀取歷史數(shù)據(jù)的方式逊桦,我們叫它快照讀 (snapshot read)眨猎,而讀取數(shù)據(jù)庫當(dāng)前版本數(shù)據(jù)的方式,叫當(dāng)前讀 (current read)卫袒。很顯然宵呛,在MVCC中:
快照讀:就是select
select * from table ….;
當(dāng)前讀:特殊的讀操作,插入/更新/刪除操作夕凝,屬于當(dāng)前讀宝穗,處理的都是當(dāng)前的數(shù)據(jù),需要加鎖码秉。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update ;
delete;
事務(wù)的隔離級別實際上都是定義了當(dāng)前讀的級別逮矛,MySQL為了減少鎖處理(包括等待其它鎖)的時間,提升并發(fā)能力转砖,引入了快照讀的概念须鼎,使得select不用加鎖鲸伴。而update、insert這些“當(dāng)前讀”晋控,就需要另外的模塊來解決了汞窗。
行鎖
MySQL中鎖的種類很多,有常見的表鎖和行鎖赡译,也有新加入的Metadata Lock等等仲吏。表鎖是對一整張表加鎖,雖然可分為讀鎖和寫鎖蝌焚,但畢竟是鎖住整張表裹唆,會導(dǎo)致并發(fā)能力下降,一般是做ddl處理時使用只洒。行鎖則是鎖住數(shù)據(jù)行许帐,這種加鎖方法比較復(fù)雜,但是由于只鎖住有限的數(shù)據(jù)毕谴,對于其它數(shù)據(jù)不加限制成畦,所以并發(fā)能力強(qiáng),MySQL一般都是用行鎖來處理并發(fā)事務(wù)涝开。
在RR級別下羡鸥,快照讀取都是不加鎖的(在Serializable隔離級別下,快照讀也會加鎖忠寻,此時不再區(qū)分快照讀和當(dāng)前讀,所有讀操作均為當(dāng)前讀)存和,但是當(dāng)前讀是需要加鎖的奕剃。通過如下實例,我們來看看行鎖(RR隔離級別):
事務(wù)A給teacher_id為1的數(shù)據(jù)行加鎖捐腿,如果一直不釋放纵朋,那么事務(wù)B將會一直等待(拿不到行鎖),直到超時茄袖。
此時需要注意一點操软,teacher_id是有索引的,如果我們將teacher_id更換為無索引的class_name宪祥,那么MySQL將會給整張表的數(shù)據(jù)行都加上行鎖聂薪。這聽起來有點不可思議,但是在SQL運行過程中蝗羊,如果一個條件無法通過索引快速過濾藏澳,存儲引擎層面就會將所有記錄加鎖后返回,再由MySQL Server層進(jìn)行過濾耀找。
但在實際使用過程當(dāng)中翔悠,MySQL做了一些改進(jìn),在MySQL Server過濾條件,發(fā)現(xiàn)不滿足后蓄愁,會調(diào)用unlock_row方法双炕,把不滿足條件的記錄釋放鎖 (違背了二段鎖協(xié)議的約束)。這樣做撮抓,保證了最后只會持有滿足條件記錄上的鎖妇斤,但是每條記錄的加鎖操作還是不能省略的≌凸觯可見即使是MySQL趟济,為了效率也是會違反規(guī)范的。所以對一個數(shù)據(jù)量很大的表做批量修改的時候咽笼,如果無法使用相應(yīng)的索引顷编,MySQL Server過濾數(shù)據(jù)的的時候特別慢,就會出現(xiàn)雖然沒有修改某些行的數(shù)據(jù)剑刑,但是它們還是被鎖住了的現(xiàn)象媳纬。
Next-Key 鎖
事務(wù)的隔離級別中雖然只定義了讀數(shù)據(jù)的要求,實際上這也可以說是寫數(shù)據(jù)的要求施掏,為了解決當(dāng)前讀中的幻讀問題钮惠,MySQL事務(wù)使用了Next-Key鎖。Next-Key鎖是行鎖和GAP(間隙鎖)的合并七芭。行鎖可以防止不同事務(wù)版本的數(shù)據(jù)修改提交時造成數(shù)據(jù)沖突的情況素挽。但如何避免別的事務(wù)插入數(shù)據(jù)就成了問題。為了理解GAP(間隙鎖)狸驳,我們看下RR和RC級別的對比:
RC級別:
RR級別:
通過RC和RR的對比我們發(fā)現(xiàn)预明,RC隔離級別下,發(fā)生了當(dāng)前讀的幻讀耙箍,事務(wù)A在update之后撰糠,將事務(wù)B insert的數(shù)據(jù)一并查詢了出來,并且是還未被事務(wù)A的update語句修改的數(shù)據(jù)(RC隔離級別下辩昆,當(dāng)前讀仍然會有可能發(fā)生幻讀現(xiàn)象)阅酪。
而RR隔離級別下,事務(wù)A在update之后汁针,事務(wù)B無法insert术辐,事務(wù)A在update前后讀的數(shù)據(jù)保持一致,避免了幻讀施无。而使事務(wù)B無法insert的這個鎖术吗,就是GAP(間隙鎖)。在class_teacher這張表中帆精,teacher_id是個索引较屿,那么它就會維護(hù)一套B+樹的數(shù)據(jù)關(guān)系隧魄,為了簡化,我們用鏈表結(jié)構(gòu)來表達(dá)(實際上是個樹形結(jié)構(gòu)隘蝎,但原理相同)
如圖所示购啄,InnoDB使用的是聚集索引,teacher_id身為二級索引嘱么,就要維護(hù)一個索引字段和主鍵id的樹狀結(jié)構(gòu)(這里用鏈表形式表現(xiàn))狮含,并保持順序排列。InnoDB將這段數(shù)據(jù)分成幾個個區(qū)間
(negative infinity, 5],
(5,30],
(30,positive infinity)曼振;
update class_teacher set class_name=‘初三四班’ where teacher_id=30;不僅用行鎖几迄,鎖住了相應(yīng)的數(shù)據(jù)行;同時也在兩邊的區(qū)間冰评,(5,30]和(30映胁,positive infinity),都加入了GAP(間隙鎖)甲雅。這樣事務(wù)B就無法在這個兩個區(qū)間insert進(jìn)新數(shù)據(jù)解孙。受限于這種實現(xiàn)方式,InnoDB很多時候會鎖住不需要鎖的區(qū)間抛人。如下所示:
teacher_id=20是在(5弛姜,30]區(qū)間,即使沒有修改任何數(shù)據(jù)妖枚,InnoDB也會在這個區(qū)間加GAP(間隙鎖)廷臼,而其它區(qū)間不會影響,事務(wù)C正常插入绝页。
如果使用的是沒有索引的字段中剩,比如update class_teacher set teacher_id=7 where class_name=‘初三八班(即使沒有匹配到任何數(shù)據(jù))’,那么會給全表加入GAP(間隙鎖)。同時抒寂,它不能像上文中行鎖一樣經(jīng)過MySQL Server過濾自動解除不滿足條件的鎖,因為沒有索引掠剑,則這些字段也就沒有排序屈芜,也就沒有區(qū)間。除非該事務(wù)提交朴译,否則其它事務(wù)無法插入任何數(shù)據(jù)井佑。
行鎖防止別的事務(wù)修改或刪除,GAP鎖防止別的事務(wù)新增眠寿,行鎖和GAP鎖結(jié)合形成的的Next-Key鎖共同解決了RR級別在寫數(shù)據(jù)(當(dāng)前讀)時的幻讀問題躬翁。
GAP(間隙鎖)RR隔離級別的當(dāng)前讀加鎖分析
WHERE字段無索引
無索引的條件字段的當(dāng)前讀不僅會把每條記錄都加上行(X)鎖,還會加上GAP鎖盯拱。再次強(qiáng)調(diào)盒发,當(dāng)前讀或者插入/更新/刪除操作需要加上索引例嘱。
WHERE字段存在普通索引
普通索引的條件字段的當(dāng)前讀會把符合檢索條件的每條記錄加上行(X)鎖,還會在索引兩邊的區(qū)間加上對應(yīng)的GAP鎖宁舰。
WHERE字段存在唯一索引
唯一索引的條件字段的當(dāng)前讀只會為符合檢索條件的那條記錄加上行(X)鎖拼卵,不會加上GAP鎖。
七蛮艰、InnoDB在RR級別下能否防止幻讀
經(jīng)過上面的描述腋腮,我們可以得出如下結(jié)論:
在RR隔離級別下,只進(jìn)行快照讀(不進(jìn)行當(dāng)前讀)壤蚜,是能夠防止幻讀的即寡。但是可能會出現(xiàn)P4(Lost To Modify 丟失修改)現(xiàn)象,而針對MVCC實現(xiàn)定義的Snapshot Isolation袜刷,其實是不允許P4現(xiàn)象的聪富,出現(xiàn)更新沖突,遵循FIRST-COMMITER-WIN的原則水泉,其它事務(wù)需要回滾善涨。
在RR隔離級別下,只進(jìn)行當(dāng)前讀(SELECT使用排它鎖(不使用共享鎖))草则,也能夠防止幻讀(Next-Key Lock解決幻讀問題)钢拧,并且不會出現(xiàn)P4(Lost To Modify 丟失修改)現(xiàn)象。
在RR隔離級別下炕横,如果先快照讀源内、后進(jìn)行當(dāng)前讀(指的是后續(xù)進(jìn)行UPDATE、DELETE份殿,先快照讀和后面使用鎖定讀饶さ觥(如FOR UPDATE)兩次結(jié)果不一樣,不在討論范圍中卿嘲,個人認(rèn)為此種情況不能用來證明InnoDB不能防止幻讀)颂斜,是無法防止幻讀。MySQL官方對此種情況的答復(fù)是:如果一個事務(wù)確實更新或者刪除了其他事務(wù)提交的行拾枣,那么這些更改對當(dāng)前事務(wù)是可見的(If a transaction does update or delete rows committed by a different transaction, those changes do become visible to the current transaction)沃疮。我們從官方的答復(fù)中得知,如果先進(jìn)行快照讀梅肤,然后在快照讀中執(zhí)行更新或者刪除司蔬,而其他事務(wù)在快照讀之后(本事務(wù)執(zhí)行更新前)對當(dāng)前事務(wù)要更新的行也進(jìn)行了更新,那么其他事務(wù)的對滿足條件的行的更新對當(dāng)前事務(wù)是可見的姨蝴。
ANSI SQL STANDARD對于可重讀隔離級別俊啼,是允許出現(xiàn)幻讀的,但是MySQL可以通過Next-Key Lock來解決幻讀問題左医,當(dāng)然授帕,如果只進(jìn)行純快照讀也能避免幻讀同木。