面試官:談一談你對MySQL中的事物和MVCC的理解器罐?
我:吧啦吧啦說了下面一堆,不過看面試官的臉色應(yīng)該是被驚喜到了渐行。
為什么要有事務(wù)
說到事務(wù)轰坊,不得不提到轉(zhuǎn)賬的事情,幾乎所有的關(guān)于事務(wù)的文章都會提到這個老掉牙的案例祟印,我也不例外肴沫。
轉(zhuǎn)賬在數(shù)據(jù)庫層面可以簡單的抽象成兩個部分:
- 從自己的賬戶中扣除轉(zhuǎn)賬金額;
- 往對方賬戶中增加轉(zhuǎn)賬金額蕴忆。
如果先從自己的賬戶中扣除轉(zhuǎn)賬金額颤芬,再往對方賬戶中增加轉(zhuǎn)賬金額,扣除執(zhí)行成功套鹅,增加執(zhí)行失敗站蝠,那自己的賬戶白白少了100塊,欲哭無淚卓鹿。
如果先往對方賬戶中增加轉(zhuǎn)賬金額菱魔,再從自己的賬戶中扣除轉(zhuǎn)賬金額,增加執(zhí)行成功吟孙,扣除執(zhí)行失敗澜倦,那對方賬戶白白增加了100塊,自己的賬戶也沒有扣錢杰妓,喜大普奔肥隆。
不管是讓你欲哭無淚,還是喜大普奔稚失,銀行都不會容忍這樣的事情發(fā)生栋艳,他們會引入事務(wù)來解決這類問題。
事務(wù)的特性
- 原子性(Atomicity):事務(wù)包含的所有操作要么全部成功(提交)句各,要么全部失斘肌(回滾)晴叨。
- 一致性(Consistency):事務(wù)的執(zhí)行的前后數(shù)據(jù)的完整性保持一致。
- 隔離性(Isolation):一個事務(wù)執(zhí)行的過程中矾屯,不應(yīng)該受到其他事務(wù)的干擾兼蕊。
- 持久性(Durability):事務(wù)一旦結(jié)束,數(shù)據(jù)就持久到數(shù)據(jù)庫件蚕,即使提交后孙技,數(shù)據(jù)庫發(fā)生崩潰,也不會丟失提交的數(shù)據(jù)排作。
四種特性牵啦,簡稱ACID,其中最不好理解的就是一致性妄痪,有不少人認(rèn)為原子性哈雏、隔離性、持久性就是為了保證一致性衫生,我們也不搞學(xué)術(shù)研究裳瘪,一致性到底該怎么解釋,到底怎么定義一致性罪针,就看各位看官的了彭羹。
事務(wù)的隔離級別
從某個角度來說,我們可以控制的泪酱、或者說需要研究的只有隔離性這一個特性派殷,而要控制隔離性,幾乎只有調(diào)整隔離級別這一個手段西篓,下面我們就來看看事務(wù)的隔離級別愈腾。
數(shù)據(jù)庫是一個客戶端/服務(wù)器架構(gòu)的軟件憋活,每個客戶端與服務(wù)器連接后岂津,就會產(chǎn)生一個session(會話),客戶端和服務(wù)器的交互就是在session中進(jìn)行的悦即,理論上來說吮成,如果服務(wù)器同時只能處理一個事務(wù),其他的事務(wù)都排隊等待辜梳,當(dāng)該事務(wù)提交后粱甫,服務(wù)器才處理下一個事務(wù),這樣才真正具有“隔離性”作瞄,什么問題都沒有了茶宵,但是如果是這樣,性能就太差了宗挥,在性能和隔離性之間乌庶,只能做一些平衡种蝶,所以數(shù)據(jù)庫提供了好幾個隔離級別供我們選擇。
在講隔離級別之前瞒大,我們先來看看事務(wù)并發(fā)執(zhí)行會遇到什么問題螃征。
為了保證下面的敘述可以順利進(jìn)行,我們要先建一張表:
CREATE TABLE `student` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL COMMENT '姓名',
`age` int(11) DEFAULT NULL COMMENT '年齡',
`grade` int(11) DEFAULT NULL COMMENT '年級',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
臟寫
- sessionA和sessionB開啟了一個事務(wù)透敌;
- sessionB把id=2的name修改成了“地底王”盯滚;
- sessionA把id=2的name修改成了“夢境地底王”;
- sessionB回滾了事務(wù)酗电;
- sessionA提交了事務(wù)魄藕。
如果sessionB在回滾事務(wù)的時候把sessionA的修改也給回滾了,導(dǎo)致sessionA的提交丟失了顾瞻,這種現(xiàn)象就被稱為“臟寫”泼疑。sessionA會一臉懵逼,我明明修改了數(shù)據(jù)荷荤,也提交了數(shù)據(jù)退渗,為什么數(shù)據(jù)沒有變化呢。
臟讀
- sessionA和sessionB開啟了一個事務(wù)蕴纳;
- sessionB把id=2的name修改成了“地底王”会油,此時還未提交;
- sessionA查詢了id=2的數(shù)據(jù)古毛,如果讀出來的數(shù)據(jù)的name是“地底王”翻翩,也就是讀到了sessionB還沒有提交的數(shù)據(jù),就被稱為“臟讀”稻薇。
不可重復(fù)讀
- sessionA和sessionB開啟了一個事務(wù)嫂冻;
- sessionA查詢id=2的數(shù)據(jù),假如name是“地底王”塞椎,
- sessionB把id=2的name修改成了“夢境地底王”桨仿,隨后提交了事務(wù);
- sessionA再一次查詢了id=2的數(shù)據(jù)案狠,如果name是“夢境地底王”服傍,說明在同一個事務(wù)中,sessionA前后讀到的數(shù)據(jù)不一致骂铁,就被稱為“不可重復(fù)讀”吹零。
幻讀
- sessionA和sessionB開啟了一個事務(wù);
- sessionA查詢name=“地底王”的數(shù)據(jù)拉庵,假設(shè)此時讀到了一條記錄灿椅;
- sessionB又插入一條name=“地底王”的數(shù)據(jù),隨后提交;
- seesionA再一次查詢name=“地底王”的數(shù)據(jù)茫蛹,如果此時讀到了兩條記錄泣懊,第二次查詢讀到了第一次查詢未查詢出來的數(shù)據(jù),就被稱為“幻讀”麻惶。
四種隔離級別
我們知道了在并發(fā)執(zhí)行事務(wù)的時候馍刮,會遇到什么問題,有些問題比較嚴(yán)重窃蹋,有些問題比較輕微卡啰,一般來說,我們認(rèn)為按照嚴(yán)重性排序是這樣的:
臟寫>臟讀>不可重復(fù)讀>幻讀
在SQL標(biāo)準(zhǔn)定義中警没,設(shè)定了四種隔離級別匈辱,來解決上述的問題:
- 未提交讀(READ UNCOMMITTED): 最低的隔離級別,會有“臟讀”杀迹、“不可重復(fù)讀”亡脸,“幻讀”三個問題。
- 讀已提交(READ COMMITTED): SQLServer默認(rèn)隔離級別树酪,可以避免“臟讀”浅碾,會有“不可重復(fù)讀”,“幻讀”兩個問題续语。
- 可重復(fù)讀(REPEATABLE READ): 可以避免“臟讀”垂谢,“不可重復(fù)讀”兩個問題,會有“幻讀”問題疮茄。 MySQL默認(rèn)隔離級別滥朱,但是在MySQL中,此隔離級別解決了“幻讀”問題力试。
- 串行化(SERIALIZABLE): 所有的問題都不會發(fā)生徙邻。
因為臟寫的問題實在太嚴(yán)重了,在任何隔離級別下畸裳,都不會有臟寫的問題缰犁。
MVCC
前面說的都是開胃菜,相信大部分小伙伴對于上述內(nèi)容都是手到擒來躯畴,所以我連如何修改事務(wù)隔離級別都沒有介紹民鼓,各種實驗也都沒有做薇芝,就是要把大量的時間蓬抄、文字投入到這一部分內(nèi)容中來。
MVCC夯到,全稱是Mutil-Version Concurrency Control嚷缭,翻譯成中文是多版本并發(fā)控制,MySQL就利用了MVCC來判斷在一個事務(wù)中,哪個數(shù)據(jù)可以被讀出來阅爽,哪個數(shù)據(jù)不能被讀出來路幸。
多版本
在看MVCC之前,我們有必要知道另外一個知識點付翁,數(shù)據(jù)庫存儲一行行數(shù)據(jù)简肴,是分為兩個部分來存儲的诡右,一個是數(shù)據(jù)行的額外信息(本篇博客不涉及)彻秆,一個是真實的數(shù)據(jù)記錄,MySQL會為每一行真實數(shù)據(jù)記錄添加兩三個隱藏的字段:
- row_id 非必須块茁,如果表中有自定義的主鍵或者有Unique鍵佣渴,就不會添加row_id字段辫狼,如果兩者都沒有,MySQL會“自作主張”添加row_id字段辛润。
- transaction_id 必須膨处,事務(wù)Id,代表這一行數(shù)據(jù)是由哪個事務(wù)id創(chuàng)建的砂竖。
- roll_pointer 必須真椿,回滾指針,指向這行數(shù)據(jù)的上一個版本乎澄。
如下圖所示:
在這里需要著重說明下事務(wù)id瀑粥,當(dāng)我們開啟一個事務(wù),并不會馬上獲得事務(wù)id三圆,哪怕我們在事務(wù)中執(zhí)行select語句狞换,也是沒有事務(wù)id的(事務(wù)id為0),只有執(zhí)行insert/update/delete語句才能獲得事務(wù)id舟肉,這一點尤為重要修噪。
其中和MVCC緊密相關(guān)的是transaction_id和roll_pointer兩個字段,在開發(fā)過程中路媚,我們無需關(guān)心黄琼,但是要研究MVCC,我們必須關(guān)心整慎。
如果有類似這樣的一行數(shù)據(jù):
代表這行數(shù)據(jù)是由transaction_id為9的事務(wù)創(chuàng)建出來的脏款,roll_pointer是空的,因為這是一條新紀(jì)錄裤园。
實際上撤师,roll_pointer并不是空的,如果真要解釋拧揽,需要繞一大圈剃盾,理解成空的腺占,問題也不大。
當(dāng)我們開啟事務(wù)痒谴,對這條數(shù)據(jù)進(jìn)行修改衰伯,會變成這樣:
有點感覺了吧,這就像一個單向鏈表积蔚,稱之為“版本鏈”意鲸,最上面的數(shù)據(jù)是這個數(shù)據(jù)的最新版本,roll_pointer指向這個數(shù)據(jù)的舊版本尽爆,給人的感覺就是一行數(shù)據(jù)有多個版本临扮,是不是符合“多版本并發(fā)控制”中的“多版本”這個概念, 那么“并發(fā)控制”又是怎么做到的呢教翩,別急杆勇,繼續(xù)往下看。
ReadView
哎饱亿,下面又要引出一個新的概念:ReadView蚜退。
對于READ UNCOMMITTED來說,可以讀取到其他事務(wù)還沒有提交的數(shù)據(jù)彪笼,所以直接把這個數(shù)據(jù)的最新版本讀出來就可以了钻注,對于SERIALIZABLE來說,是用加鎖的方式來訪問記錄配猫。
剩下的就是READ COMMITTED和REPEATABLE READ幅恋,這兩個事務(wù)隔離級別都要保證讀到的數(shù)據(jù)是其他事務(wù)已經(jīng)提交的,也就是不能無腦把一行數(shù)據(jù)的最新版本給讀出來了泵肄,但是這兩個還是有一定的區(qū)別捆交,最核心的問題就在于“我到底可以讀取這個數(shù)據(jù)的哪個版本”。
為了解決這個問題腐巢,ReadView的概念就出現(xiàn)了品追,ReadView包含四個比較重要的內(nèi)容:
- m_ids:表示在生成ReadView時,系統(tǒng)中活躍的事務(wù)id集合冯丙。
- min_trx_id:表示在生成ReadView時肉瓦,系統(tǒng)中活躍的最小事務(wù)id,也就是 m_ids中的最小值胃惜。
- max_trx_id:表示在生成ReadView時泞莉,系統(tǒng)應(yīng)該分配給下一個事務(wù)的id。
- creator_trx_id:表示生成該ReadView的事務(wù)id船殉。
有了這個ReadView鲫趁,只要按照下面的判斷方式就可以解決“我到底可以讀取這個數(shù)據(jù)的哪個版本”這個千古難題了:
- 如果被訪問的版本的trx_id和ReadView中的creator_trx_id相同,就意味著當(dāng)前版本就是由你“造成”的捺弦,可以讀出來饮寞。
- 如果被訪問的版本的trx_id小于ReadView中的min_trx_id,表示生成該版本的事務(wù)在創(chuàng)建ReadView的時候列吼,已經(jīng)提交了幽崩,所以該版本可以讀出來。
- 如果被訪問版本的trx_id大于或等于ReadView中的max_trx_id值寞钥,說明生成該版本的事務(wù)在當(dāng)前事務(wù)生成ReadView后才開啟慌申,所以該版本不可以被讀出來。
- 如果生成被訪問版本的trx_id在min_trx_id和max_trx_id之間理郑,那就需要判斷下trx_id在不在m_ids中:如果在蹄溉,說明創(chuàng)建ReadView的時候,生成該版本的事務(wù)還是活躍的(沒有被提交)您炉,該版本不可以被讀出來柒爵;如果不在,說明創(chuàng)建ReadView的時候赚爵,生成該版本的事務(wù)已經(jīng)被提交了棉胀,該版本可以被讀出來。
如果某個數(shù)據(jù)的最新版本不可以被讀出來冀膝,就順著roll_pointer找到該數(shù)據(jù)的上一個版本唁奢,繼續(xù)做如上的判斷,以此類推窝剖,如果第一個版本也不可見的話麻掸,代表該數(shù)據(jù)對當(dāng)前事務(wù)完全不可見,查詢結(jié)果就不包含這條記錄了赐纱。
看完上面的描述脊奋,是不是覺得“云里霧里”,“不知所云”疙描,甚至“腦闊疼狂魔,整個人都不好了”。
我們換個方法來解釋淫痰,看會不會更容易理解點:
在事務(wù)啟動的一瞬間最楷,會創(chuàng)建出ReadView,對于一個數(shù)據(jù)版本的trx_id來說待错,有以下三種情況:
- 如果落在低水位籽孙,表示生成這個版本的事務(wù)已經(jīng)提交了,或者是當(dāng)前事務(wù)自己生成的火俄,這個版本可見犯建。
- 如果落在高水位,表示生成這個版本的事務(wù)是未來才創(chuàng)建的瓜客,這個版本不可見适瓦。
- 如果落在中間水位竿开,包含兩種情況: a. 如果當(dāng)前版本的trx_id在活躍事務(wù)列表中,代表這個版本是由還沒有提交的事務(wù)生成的玻熙,這個版本不可見否彩; b. 如果當(dāng)前版本的trx_id不在活躍事務(wù)列表中,代表這個版本是由已經(jīng)提交的事務(wù)生成的嗦随,這個版本可見列荔。
上面我比較簡單的解釋了下ReadView,用了兩種方式來說明如何判斷當(dāng)前數(shù)據(jù)版本是否可見枚尼,不知道各位看官是不是有了一個比較模糊的概念贴浙,有了ReadView的基本概念,我們就可以具體看下READ COMMITTED署恍、REPEATABLE READ這兩個事務(wù)隔離級別為什么讀到的數(shù)據(jù)是不同的崎溃,以及上述規(guī)則是如何應(yīng)用的。
READ COMMITTED——每次讀取數(shù)據(jù)都會創(chuàng)建ReadView
假設(shè)盯质,現(xiàn)在系統(tǒng)只有一個活躍的事務(wù)T笨奠,事務(wù)id是100,事務(wù)中修改了數(shù)據(jù)唤殴,但是還沒有提交般婆,形成的版本鏈?zhǔn)沁@樣的:
現(xiàn)在A事務(wù)啟動,并且執(zhí)行了select語句朵逝,此時會創(chuàng)建出一個ReadView蔚袍,m_ids是【100】,min_trx_id是100配名, max_trx_id是101啤咽,creator_trx_id是0。
為什么m_ids只有一個渠脉,為什么creator_trx_id是0宇整?這里再次強(qiáng)調(diào)下,只有在事務(wù)中執(zhí)行insert/update/delete語句才能獲得事務(wù)id芋膘。
那么A事務(wù)執(zhí)行的select語句會讀到什么數(shù)據(jù)呢鳞青?
- 判斷最新的數(shù)據(jù)版本,name是“夢境地底王”为朋,對應(yīng)的trx_id是100臂拓,trx_id在m_ids里面,說明當(dāng)前事務(wù)是活躍事務(wù)习寸,這個數(shù)據(jù)版本是由還沒有提交的事務(wù)創(chuàng)建的胶惰,所以這個版本不可見。
- 順著roll_pointer找到這個數(shù)據(jù)的上一個版本霞溪,name是“地底王”孵滞,對應(yīng)的trx_id是99中捆,而ReadView中的min_trx_id是100,trx_id<min_trx_id坊饶,代表當(dāng)前數(shù)據(jù)版本是由已經(jīng)提交的事務(wù)創(chuàng)建的泄伪,該版本可見。
所以讀到的數(shù)據(jù)的name是“地底王”幼东。
我們把事務(wù)T提交了臂容,事務(wù)A再次執(zhí)行select語句科雳,此時根蟹,事務(wù)A再次創(chuàng)建出ReadView,m_ids是【】糟秘,min_trx_id是0简逮, max_trx_id是101,creator_trx_id是0尿赚。
因為事務(wù)T已經(jīng)提交了散庶,所以沒有活躍的事務(wù)。
那么事務(wù)A第二次執(zhí)行select語句又會讀到什么數(shù)據(jù)呢凌净?
- 判斷最新的數(shù)據(jù)版本悲龟,name是“夢境地底王”,對應(yīng)的trx_id是100冰寻,不在m_ids里面须教,說明這個數(shù)據(jù)版本是由已經(jīng)提交的事務(wù)創(chuàng)建的,該版本可見斩芭。
所以讀到的數(shù)據(jù)的name是“夢境地底王”轻腺。
REPEATABLE READ ——首次讀取數(shù)據(jù)會創(chuàng)建ReadView
假設(shè),現(xiàn)在系統(tǒng)只有一個活躍的事務(wù)T划乖,事務(wù)id是100贬养,事務(wù)中修改了數(shù)據(jù),但是還沒有提交琴庵,形成的版本鏈?zhǔn)沁@樣的:
現(xiàn)在A事務(wù)啟動误算,并且執(zhí)行了select語句,此時會創(chuàng)建出一個ReadView迷殿,m_ids是【100】,min_trx_id是100贪庙, max_trx_id是101,creator_trx_id是0止邮。
那么A事務(wù)執(zhí)行的select語句會讀到什么數(shù)據(jù)呢奏窑?
- 判斷最新的數(shù)據(jù)版本,name是“夢境地底王”屈扎,對應(yīng)的trx_id是100,trx_id在m_ids里面鹰晨,說明當(dāng)前事務(wù)是活躍事務(wù),這個數(shù)據(jù)版本是由還沒有提交的事務(wù)創(chuàng)建的模蜡,所以這個版本不可見漠趁。
- 順著roll_ponit找到這個數(shù)據(jù)的上一個版本,name是“地底王”忍疾,對應(yīng)的trx_id是99,而ReadView中的min_trx_id是100甥绿,trx_id<min_trx_id则披,代表當(dāng)前數(shù)據(jù)版本是由已經(jīng)提交的事務(wù)創(chuàng)建的,該版本可見士复。
所以讀到的數(shù)據(jù)的name是“地底王”。
細(xì)心的你判没,一定發(fā)現(xiàn)了,這里我就是復(fù)制粘貼嫉沽,因為在REPEATABLE READ事務(wù)隔離級別下俏竞,事務(wù)A首次執(zhí)行select語句創(chuàng)建出來的ReadView和在READ COMMITTED事務(wù)隔離級別下,事務(wù)A首次執(zhí)行select語句創(chuàng)建出來的ReadView是一樣的玻佩,所以判斷流程也是一樣的席楚,所以我就偷懶了,copy走起。
隨后郎仆,事務(wù)T提交了事務(wù)兜蠕,由于REPEATABLE READ是首次讀取數(shù)據(jù)才會創(chuàng)建ReadView,所以事務(wù)A再次執(zhí)行select語句熊杨,不會再創(chuàng)建ReadView曙旭,用的還是上一次的ReadView晶府,所以判斷流程和上面也是一樣的,所以讀到的name還是“地底王”郊霎。
最后
為大家整理了MySQL的有關(guān)知識(PDF)书劝,分為基礎(chǔ)篇土至,性能優(yōu)化篇以及架構(gòu)設(shè)計篇。希望大家看完后能理解透MySQL的知識陶因,面試不必愁。
領(lǐng)取方式:關(guān)注并轉(zhuǎn)發(fā)解幽,后臺私信【資料】或點擊右方鏈接:https://shimo.im/docs/QVy8HrQgPYkx9Ddg/即可免費(fèi)領(lǐng)取烘苹。
作者:CoderBear
鏈接:https://juejin.im/post/5e97d6b7e51d4546f5790f7e
來源:掘金