上一篇我們簡(jiǎn)單聊了聊MySQL中LRU算法的實(shí)現(xiàn),那么這一篇我們聊聊MySQL的另一個(gè)重點(diǎn)——MVCC(多版本并發(fā)控制)嘹黔;
一峭状、什么是MVCC
???? MVCC,全稱Multi-Version Concurrency Control泊窘,即多版本并發(fā)控制熄驼。MVCC是一種并發(fā)控制的方法像寒,一般在數(shù)據(jù)庫管理系統(tǒng)中,實(shí)現(xiàn)對(duì)數(shù)據(jù)庫的并發(fā)訪問瓜贾,在編程語言中實(shí)現(xiàn)事務(wù)內(nèi)存诺祸。
? ? MVCC在MySQL InnoDB中的實(shí)現(xiàn)主要是為了提高數(shù)據(jù)庫并發(fā)性能,用更好的方式去處理讀-寫沖突祭芦,做到即使有讀寫沖突時(shí)筷笨,也能做到不加鎖,非阻塞并發(fā)讀龟劲。
????舉個(gè)例子胃夏,程序員A正在讀數(shù)據(jù)庫中某些內(nèi)容,而程序員B正在給這些內(nèi)容做修改(假設(shè)是在一個(gè)事務(wù)內(nèi)修改昌跌,大概持續(xù)10s左右)仰禀,A在這10s內(nèi) 則可能看到一個(gè)不一致的數(shù)據(jù),在B沒有提交前蚕愤,如何讓A能夠一直讀到的數(shù)據(jù)都是一致的呢
有幾種處理方法:
????第一種:基于鎖的并發(fā)控制答恶,程序員B開始修改數(shù)據(jù)時(shí),給這些數(shù)據(jù)加上鎖审胸,程序員A這時(shí)再讀亥宿,就發(fā)現(xiàn)讀取不了,處于等待情況砂沛,只能等B操作完才能讀數(shù)據(jù)烫扼,這保證A不會(huì)讀到一個(gè)不一致的數(shù)據(jù),但是這個(gè)會(huì)影響程序的運(yùn)行效率碍庵。
????還有一種就是:MVCC映企,每個(gè)用戶連接數(shù)據(jù)庫時(shí),看到的都是某一特定時(shí)刻的數(shù)據(jù)庫快照静浴,在B的事務(wù)沒有提交之前堰氓,A始終讀到的是某一特定時(shí)刻的數(shù)據(jù)庫快照,不會(huì)讀到B事務(wù)中的數(shù)據(jù)修改情況苹享,直到B事務(wù)提交双絮,才會(huì)讀取B的修改內(nèi)容。
二得问、什么是當(dāng)前讀和快照讀
在學(xué)習(xí)MVCC多版本并發(fā)控制之前囤攀,我們必須先了解一下,什么是MySQL InnoDB下的當(dāng)前讀和快照讀?
????當(dāng)前讀:
? ? ? ? 像select 語句 :lock in share mode(共享鎖), select 語句 for update ; update, insert ,delete(排他鎖)這些操作都是一種當(dāng)前讀宫纬,為什么叫當(dāng)前讀焚挠?就是它讀取的是記錄的最新版本,讀取時(shí)還要保證其他并發(fā)事務(wù)不能修改當(dāng)前記錄漓骚,會(huì)對(duì)讀取的記錄進(jìn)行加鎖蝌衔。
? ? 快照讀:
? ? ? ? 像不加鎖的select * from 操作就是快照讀榛泛,即不加鎖的非阻塞讀,不涉及其他鎖之間的沖突噩斟;快照讀的前提是隔離級(jí)別不是串行級(jí)別曹锨,串行級(jí)別下的快照讀會(huì)退化成當(dāng)前讀;之所以出現(xiàn)快照讀的情況剃允,是基于提高并發(fā)性能的考慮艘希,快照讀的實(shí)現(xiàn)是基于多版本并發(fā)控制,即MVCC,可以認(rèn)為MVCC是行鎖的一個(gè)變種硅急,但它在很多情況下,避免了加鎖操作佳遂,降低了開銷营袜;既然是基于多版本,即快照讀可能讀到的并不一定是數(shù)據(jù)的最新版本丑罪,而有可能是之前的歷史版本荚板。
?????說白了MVCC就是為了實(shí)現(xiàn)讀(select)-寫沖突不加鎖,而這個(gè)讀指的就是快照讀, 而非當(dāng)前讀吩屹,當(dāng)前讀實(shí)際上是一種加鎖的操作跪另,是悲觀鎖的實(shí)現(xiàn)。
三煤搜、MVCC的實(shí)現(xiàn)原理
????MVCC的目的就是多版本并發(fā)控制免绿,在數(shù)據(jù)庫中的實(shí)現(xiàn),就是為了解決讀寫沖突擦盾,它的實(shí)現(xiàn)原理主要是依賴記錄中的 3個(gè)隱式字段嘲驾,undo日志 ,Read View 來實(shí)現(xiàn)的迹卢。所以我們先來看看這個(gè)三個(gè)point的概念:
????每行記錄除了我們自定義的字段外辽故,還有數(shù)據(jù)庫隱式定義的DB_TRX_ID、DB_ROLL_PTR腐碱、DB_ROW_ID等字段
?DB_TRX_ID:? ? 6byte誊垢,最近修改(修改/插入)事務(wù)ID:記錄創(chuàng)建這條記錄/最后一次修改該記錄的事務(wù)ID
DB_ROLL_PTR:? ?7byte,回滾指針症见,指向這條記錄的上一個(gè)版本(存儲(chǔ)于rollback segment里)
DB_ROW_ID:? ? ? ? 6byte喂走,隱含的自增ID(隱藏主鍵),如果數(shù)據(jù)表沒有主鍵筒饰,InnoDB會(huì)自動(dòng)以DB_ROW_ID產(chǎn)生一個(gè)聚集索引
insert undo log:?代表事務(wù)在insert新記錄時(shí)產(chǎn)生的undo log, 只在事務(wù)回滾時(shí)需要缴啡,并且在事務(wù)提交后可以被立即丟棄
update undo log:? 事務(wù)在進(jìn)行update或delete時(shí)產(chǎn)生的undo log; 不僅在事務(wù)回滾時(shí)需要,在快照讀(select瓷们,當(dāng)讀的過程中有寫的事務(wù)開始和提交业栅,會(huì)造成讀數(shù)據(jù)的臟讀秒咐、不可重復(fù)讀、幻讀等)時(shí)也需要碘裕;所以不能隨便刪除携取,只有在快速讀或事務(wù)回滾不涉及該日志時(shí),對(duì)應(yīng)的日志才會(huì)被purge線程統(tǒng)一清除帮孔。
low_trx_id表示該SQL啟動(dòng)時(shí)牌废,當(dāng)前事務(wù)鏈表中最大的事務(wù)id編號(hào),也就是最近創(chuàng)建的除自身以外最大事務(wù)編號(hào)啤握;
up_trx_id表示該SQL啟動(dòng)時(shí)鸟缕,當(dāng)前事務(wù)鏈表中最小的事務(wù)id編號(hào),也就是當(dāng)前系統(tǒng)中創(chuàng)建最早但還未提交的事務(wù)排抬;
trx_ids表示所有事務(wù)鏈表中事務(wù)的id集合懂从。
undo log日志主要分為兩種:
????
purge:
????從前面的分析可以看出雷滋,為了實(shí)現(xiàn)InnoDB的MVCC機(jī)制,更新或者刪除操作都只是設(shè)置一下老記錄的deleted_bit文兢,并不真正將過時(shí)的記錄刪除晤斩。????
???為了節(jié)省磁盤空間,InnoDB有專門的purge線程來清理deleted_bit為true的記錄姆坚。為了不影響MVCC的正常工作澳泵,purge線程自己也維護(hù)了一個(gè)read view(這個(gè)read view相當(dāng)于系統(tǒng)中最老活躍事務(wù)的read view);
????如果某個(gè)記錄的deleted_bit為true,并且DB_TRX_ID相對(duì)于purge線程的read view可見兼呵,那么這條記錄一定是可以被安全清除的兔辅。
舉個(gè)例子說明下:
1、?比如一個(gè)有個(gè)事務(wù)插入person表插入了一條新記錄击喂,記錄如下维苔,name為Jerry, age為24歲,隱式主鍵是1懂昂,事務(wù)ID和回滾指針介时,我們假設(shè)為NULL
2、現(xiàn)在來了一個(gè)事務(wù)1對(duì)該記錄的name做出了修改忍法,改為Tom潮尝。
? ? 在事務(wù)1修改該行(記錄)數(shù)據(jù)時(shí),數(shù)據(jù)庫會(huì)先對(duì)該行加排他鎖饿序,然后把該行數(shù)據(jù)拷貝到undo log中勉失,作為舊記錄,既在undo log中有當(dāng)前行的拷貝副本原探;
????拷貝完畢后乱凿,修改該行name為Tom,并且修改隱藏字段的事務(wù)ID為當(dāng)前事務(wù)1的ID, 我們默認(rèn)從1開始咽弦,之后遞增徒蟆,回滾指針指向拷貝到undo log的副本記錄,既表示我的上一個(gè)版本就是它
3型型、又來了個(gè)事務(wù)2修改person表的同一個(gè)記錄段审,將age修改為30歲?
????在事務(wù)2修改該行數(shù)據(jù)時(shí),數(shù)據(jù)庫也先為該行加鎖
????然后把該行數(shù)據(jù)拷貝到undo log中闹蒜,作為舊記錄寺枉,發(fā)現(xiàn)該行記錄已經(jīng)有undo log了抑淫,那么最新的舊數(shù)據(jù)作為鏈表的表頭,插在該行記錄的undo log最前面姥闪。
????修改該行age為30歲始苇,并且修改隱藏字段的事務(wù)ID為當(dāng)前事務(wù)2的ID, 那就是2,回滾指針指向剛剛拷貝到undo log的副本記錄
????事務(wù)提交筐喳,釋放鎖催式。
四、ReadView
??? ReadView說白了就是一個(gè)數(shù)據(jù)結(jié)構(gòu)避归,在SQL開始的時(shí)候被創(chuàng)建荣月。是事務(wù)進(jìn)行快照讀(select * from)操作的時(shí)候生產(chǎn)的讀視圖(Read View),在該事務(wù)執(zhí)行的快照讀的那一刻梳毙,會(huì)生成事務(wù)系統(tǒng)當(dāng)前的一個(gè)快照喉童,記錄并維護(hù)系統(tǒng)當(dāng)前活躍事務(wù)(未提交事務(wù))的ID(當(dāng)每個(gè)事務(wù)開啟時(shí),都會(huì)被分配一個(gè)ID, 這個(gè)ID是遞增的顿天,所以最新的事務(wù),ID值越大)蔑担。
ReadView{low_trx_id, up_trx_id, trx_ids}
上述3個(gè)成員組成了ReadView中的主要部分,簡(jiǎn)單圖示如下:
????據(jù)上圖所示蹲蒲,所有數(shù)據(jù)行上DATA_TRX_ID小于up_trx_id的記錄番甩,說明修改該行的事務(wù)在當(dāng)前事務(wù)開啟之前都已經(jīng)提交完成,所以對(duì)當(dāng)前事務(wù)來說届搁,都是可見的缘薛。而對(duì)于DATA_TRX_ID大于low_trx_id的記錄,說明修改該行記錄的事務(wù)在當(dāng)前事務(wù)之后卡睦,所以對(duì)于當(dāng)前事務(wù)來說是不可見的宴胧。
????注意,ReadView是與SQL綁定的表锻,而并不是事務(wù)恕齐,所以即使在同一個(gè)事務(wù)中,每次SQL啟動(dòng)時(shí)構(gòu)造的ReadView的up_trx_id和low_trx_id也都是不一樣的瞬逊,至于DATA_TRX_ID大于low_trx_id本身出現(xiàn)也只有當(dāng)多個(gè)SQL并發(fā)的時(shí)候显歧,在一個(gè)SQL構(gòu)造完ReadView之后仪或,另外一個(gè)SQL修改了數(shù)據(jù)后又進(jìn)行了提交,對(duì)于這種情況追迟,數(shù)據(jù)其實(shí)是不可見的溶其。
????最后,至于位于(up_trx_id, low_trx_id)中間的事務(wù)是否可見敦间,這個(gè)需要根據(jù)不同的事務(wù)隔離級(jí)別來確定瓶逃。對(duì)于RC的事務(wù)隔離級(jí)別來說,對(duì)于事務(wù)執(zhí)行過程中廓块,已經(jīng)提交的事務(wù)的數(shù)據(jù)厢绝,對(duì)當(dāng)前事務(wù)是可見的,也就是說上述圖中带猴,當(dāng)前事務(wù)運(yùn)行過程中昔汉,trx1~4中任意一個(gè)事務(wù)提交,對(duì)當(dāng)前事務(wù)來說都是可見的拴清;而對(duì)于RR隔離級(jí)別來說靶病,事務(wù)啟動(dòng)時(shí),已經(jīng)開始的事務(wù)鏈表中的事務(wù)的所有修改都是不可見的口予,所以在RR級(jí)別下娄周,low_trx_id基本保持與up_trx_id相同的值即可。
最后借用一種圖來解釋MySQL中實(shí)現(xiàn)的MVCC沪停。
注:第四節(jié)插圖來源網(wǎng)易數(shù)據(jù)庫和大數(shù)據(jù)資深專家蔣鴻翔分享的文章中插圖煤辨。原發(fā)表于其個(gè)人博客?。
…………………………………分割線……………………………
不積跬步木张,無以至千里众辨;不積小流,無以成江海舷礼。
關(guān)注我鹃彻,每天分享一些小知識(shí)點(diǎn)。分享自己的小心得妻献,包含但不限于初浮声、中、高級(jí)面試題呦P荨S净印!
我都墨跡這么半天了 至朗,你不點(diǎn)關(guān)注屉符,不點(diǎn)贊,不收藏,還不轉(zhuǎn)發(fā)矗钟,你想干啥K粝恪!6滞А躬它!