InnoDB存儲(chǔ)引擎介紹
InnoDB的重要內(nèi)存結(jié)構(gòu):緩沖池
首先假設(shè)我們有一條SQL語句是這樣的:
update users set name = 'xxx' where id = 10
InnoDB存儲(chǔ)引擎中有一個(gè)非常重要的放在內(nèi)存里的組件品山,就是緩沖池(Buffer pool)诡挂,這里面會(huì)緩存很多的數(shù)據(jù)秆剪,以便于以后在查詢的時(shí)候灸促,萬一你要是內(nèi)存緩沖池里有數(shù)據(jù)乖酬,就可以不用去查磁盤了强霎。比如夏跷,當(dāng)我們的InnoDB存儲(chǔ)引擎要執(zhí)行更新語句的時(shí)候宣吱,比如對(duì)“id=10”這一行數(shù)據(jù)窃这,它其實(shí)會(huì)先看看“id=10”這一行數(shù)據(jù)是否在緩沖池里,如果不在的話征候,那么會(huì)直接從磁盤里加載到緩沖池來杭攻,而且還會(huì)對(duì)這行記錄加獨(dú)占鎖祟敛。
undo日志文件:如何讓你更新的數(shù)據(jù)可以回滾?
假設(shè)“id=10”這行數(shù)據(jù)的name原來是“zhangsan”兆解,現(xiàn)在我們要更新為“xxx”馆铁,那么此時(shí)我們得先把要更新的原來的值“zhangsan”和“id=10”這些信息,寫入到undo日志文件中去锅睛。我們大家都知道埠巨,如果執(zhí)行一個(gè)更新語句,要是它在一個(gè)事務(wù)里的話现拒,那么事務(wù)提交之前我們都是可以對(duì)數(shù)據(jù)進(jìn)行回滾的辣垒,也就是把更新為“xxx”的值回滾到之前的“zhangsan”去。所以為了考慮到未來可能要回滾數(shù)據(jù)的需要印蔬,這里會(huì)把更新前的值寫入undo日志文件勋桶,我們看下圖:
更新buffer pool中的緩存數(shù)據(jù)
當(dāng)我們把要更新的那行記錄從磁盤文件加載到緩沖池,同時(shí)對(duì)它加鎖侥猬,而且還把更新前的舊值寫入undo日志文件之后哥遮,我們就可以正式開始更新這行記錄了,更新的時(shí)候先是會(huì)更新緩沖池中的記錄陵究,此時(shí)這個(gè)數(shù)據(jù)就是臟數(shù)據(jù)眠饮。這里所謂的更新內(nèi)存緩沖池里的數(shù)據(jù),意思就是把內(nèi)存里的“id=10”這行數(shù)據(jù)的name字段修改為“xxx”铜邮,那為什么說此時(shí)這行數(shù)據(jù)就是臟數(shù)據(jù)了呢仪召?因?yàn)檫@個(gè)時(shí)候磁盤上“id=10”這行數(shù)據(jù)的name字段還是“zhangsan”,但是內(nèi)存里這行數(shù)據(jù)已經(jīng)被修改了松蒜,所以就會(huì)叫它是臟數(shù)據(jù)扔茅。
Redo Log Buffer:萬一系統(tǒng)宕機(jī),如何避免數(shù)據(jù)丟失秸苗?
我們現(xiàn)在思考這樣一個(gè)問題召娜,按照上面的描述,現(xiàn)在已經(jīng)把內(nèi)存里的數(shù)據(jù)進(jìn)行了修改惊楼,但是磁盤上的數(shù)據(jù)還沒修改玖瘸,那么此時(shí)萬一MySQL所在的機(jī)器宕機(jī)了,必然會(huì)導(dǎo)致內(nèi)存里修改過的數(shù)據(jù)丟失檀咙,這怎么辦呢雅倒?這個(gè)時(shí)候,就必須要把對(duì)內(nèi)存所做的修改寫入到一個(gè)Redo Log Buffer里去弧可,這也是內(nèi)存里的一個(gè)緩沖區(qū)蔑匣,是用來存放redo日志的。所謂的redo日志,就是記錄下來你對(duì)數(shù)據(jù)做了什么修改裁良,比如對(duì)“id=10”這行記錄修改了name字段的值為“xxx”凿将,這就是一個(gè)日志。我先看下圖的示意:
這個(gè)redo日志其實(shí)是用來在MySQL突然宕機(jī)的時(shí)候价脾,用來恢復(fù)更新過的數(shù)據(jù)的丸相,但是我們現(xiàn)在還沒法直接講解redo是如何做的,畢竟現(xiàn)在redo日志還僅僅停留在內(nèi)存緩沖里彼棍。
如果還沒提交事務(wù),MySQL宕機(jī)了怎么辦膳算?
我都知道在數(shù)據(jù)庫中座硕,哪怕執(zhí)行一條SQL語句,其實(shí)也可以是一個(gè)獨(dú)立的事務(wù)涕蜂,只有當(dāng)你提交事務(wù)之后华匾,SQL語句才算執(zhí)行結(jié)束。到目前為止机隙,其實(shí)還沒有提交事務(wù)蜘拉,那么此時(shí)如果MySQL服務(wù)器宕機(jī),必然導(dǎo)致內(nèi)存里Buffer Pool中的修改過的數(shù)據(jù)都丟失有鹿,同時(shí)寫入Redo Log Buffer中的redo日志也會(huì)丟失旭旭。那么此時(shí)數(shù)據(jù)丟失要緊嗎?其實(shí)是不要緊的葱跋,因?yàn)槟阋粭l更新語句持寄,沒提交事務(wù),就代表它沒執(zhí)行成功娱俺,此時(shí)MySQL宕機(jī)雖然導(dǎo)致內(nèi)存里的數(shù)據(jù)都丟失了稍味,但是你會(huì)發(fā)現(xiàn),磁盤上的數(shù)據(jù)依然還停留在原樣子荠卷。所以此時(shí)你的這個(gè)事務(wù)就是執(zhí)行失敗了模庐,你會(huì)收到一個(gè)數(shù)據(jù)庫的異常。然后MySQL重啟之后油宜,你會(huì)發(fā)現(xiàn)你的數(shù)據(jù)并沒有任何變化掂碱。
提交事務(wù)的時(shí)候?qū)edo日志寫入磁盤中
現(xiàn)在我們要提交一個(gè)事務(wù)了,此時(shí)就會(huì)根據(jù)一定的策略把redo日志從redo log buffer里刷入到磁盤文件里慎冤。此時(shí)這個(gè)策略是通過innodb_flush_log_at_trx_commit來配置的顶吮,它有幾個(gè)選項(xiàng):
1、innodb_flush_log_at_trx_commit=0粪薛,提交事務(wù)的時(shí)候不會(huì)把redo log buffer里的數(shù)據(jù)刷入磁盤文件悴了,如果MySQL宕機(jī),此時(shí)內(nèi)存里的數(shù)據(jù)全部丟失。相當(dāng)于事務(wù)提交成功了湃交,但是由于MySQL突然宕機(jī)熟空,導(dǎo)致內(nèi)存中的數(shù)據(jù)和redo日志都丟失。
2搞莺、innodb_flush_log_at_trx_commit=1息罗,提交事務(wù)的時(shí)候就必須把redo log buffer從內(nèi)存刷入到磁盤文件里去,只要事務(wù)提交成功才沧,那么redo log就必然在磁盤文件里了迈喉。這個(gè)時(shí)候MySQL系統(tǒng)突然宕機(jī),此時(shí)數(shù)據(jù)會(huì)丟失嗎温圆?顯然不會(huì)挨摸,因?yàn)殡m然內(nèi)存里的修改成name=xxx的數(shù)據(jù)丟失了,但是redo日志里已經(jīng)記錄了對(duì)某某數(shù)據(jù)做了name=xxx的修改岁歉,所以重啟MySQL之后得运,它可以根據(jù)redo日志去恢復(fù)之前做過的修改。如下圖:
3锅移、innodb_flush_log_at_trx_commit=2熔掺,提交事務(wù)的時(shí)候把redo日志寫入磁盤文件對(duì)應(yīng)的os cache緩存里去,而不是直接進(jìn)入磁盤文件非剃,可能1秒后才會(huì)把os cache里的數(shù)據(jù)寫入到磁盤文件里去置逻。這種模式下摇庙,提交事務(wù)之后鞍恢,redo log可能僅僅停留在os cache內(nèi)存緩存里,沒實(shí)際進(jìn)入磁盤文件用押,萬一此時(shí)你要是機(jī)器宕機(jī)了疯坤,那么os cache里的redo log就會(huì)丟失报慕,同樣會(huì)讓你感覺提交事務(wù)了,結(jié)果數(shù)據(jù)丟了压怠。
那么三種策略怎么選擇呢眠冈?我的建議是設(shè)置為1,也就是說提交事務(wù)的時(shí)候菌瘫,redo日志必須是刷入磁盤文件的蜗顽。這樣可以嚴(yán)格保證提交事務(wù)之后,數(shù)據(jù)是絕對(duì)不會(huì)丟失的雨让,因?yàn)橛衦edo日志在磁盤文件里可以恢復(fù)你做的所有修改雇盖。當(dāng)然,如果對(duì)數(shù)據(jù)完整性要求不高的話可以選擇0或者2栖忠。
MySQL binlog到底是什么東西崔挖?
我們之前說的redo log是一種偏向物理性質(zhì)的重做日志贸街,因?yàn)樗锩嬗涗浀氖穷愃七@樣的東西,“對(duì)哪個(gè)數(shù)據(jù)頁中的什么記錄狸相,做了一個(gè)什么修改”薛匪。而binlog叫做歸檔日志,它里面記錄的是偏向于邏輯性的日志脓鹃,類似于“對(duì)users表中的id=10的一行數(shù)據(jù)做了更新操作逸尖,更新以后的值是什么”。binlog不是InnoDB存儲(chǔ)引擎特有的日志文件瘸右,是屬于MySQL server自己的日志文件娇跟。
之前我們講到太颤,在提交事務(wù)的時(shí)候會(huì)把redo log日志寫入磁盤文件里苞俘,其實(shí)在提交事務(wù)的時(shí)候我們還會(huì)把這次更新對(duì)應(yīng)的binlog日志寫入到磁盤文件中去。如下圖所示:
大家可以在這個(gè)圖里看到一些變動(dòng)栋齿,就是我們把之前跟InnoDB存儲(chǔ)引擎進(jìn)行交互的組件加入了進(jìn)來,這個(gè)組件就是之前提到的執(zhí)行器襟诸,它負(fù)責(zé)跟InnoDB進(jìn)行交互瓦堵,包括從磁盤文件里加載數(shù)據(jù)到buffer pool中進(jìn)行緩存,包括寫入undo日志歌亲,包括更新buffer pool里的數(shù)據(jù)菇用,以及寫入redo log buffer,redo log刷入磁盤陷揪,寫binlog等待惋鸥。實(shí)際上,執(zhí)行器是非常核心的一個(gè)組件悍缠,負(fù)責(zé)跟存儲(chǔ)引擎配合完成一個(gè)SQL語句在磁盤與內(nèi)存層面的全部數(shù)據(jù)更新操作卦绣。而且我們?cè)谏蠄D可以看到,我把一次更新語句的執(zhí)行飞蚓,拆分為了兩個(gè)階段滤港,上圖中的1、2趴拧、3溅漾、4幾個(gè)步驟,其實(shí)本質(zhì)是執(zhí)行這個(gè)更新語句的時(shí)候做的事著榴。然后上圖中的5和6兩個(gè)步驟添履,是從你提交事務(wù)開始的,屬于提交事務(wù)的階段了脑又。
對(duì)于binlog日志暮胧,其實(shí)也有不同的刷盤策略锐借,有一個(gè)sync_binlog參數(shù)可以控制binlog的刷盤策略,它的默認(rèn)值是0叔壤,此時(shí)你把binlog寫入磁盤的時(shí)候其實(shí)不是直接進(jìn)入磁盤文件瞎饲,而是進(jìn)入os cache內(nèi)存緩存。所以跟之前分析的一樣炼绘,如果此時(shí)機(jī)器宕機(jī)嗅战,那么你在os cache里的binlog日志是會(huì)丟失的,我們看下圖:
如果要是把sync_binlog參數(shù)設(shè)置為1的話俺亮,那么此時(shí)會(huì)強(qiáng)制在提交事務(wù)的時(shí)候驮捍,把binlog直接寫入到磁盤文件里,那么這樣提交事務(wù)之后脚曾,哪怕機(jī)器宕機(jī)东且,磁盤上的binlog也是不會(huì)丟失的。
當(dāng)我們把binlog寫入磁盤文件之后本讥,接著就會(huì)完成最終的事務(wù)提交珊泳,此時(shí)會(huì)把本次更新對(duì)應(yīng)的binlog文件名稱和binlog日志在文件里的位置都寫入到redo log文件里去,同時(shí)在redo log文件里寫入一個(gè)commit標(biāo)記拷沸。完成這個(gè)事情之后色查,才算最終完成了事務(wù)的提交。我們看下面的示意圖:
其實(shí)是用來保持redo log日志與binlog日志一致的秧了。我們來舉個(gè)例子,假設(shè)我們?cè)谔峤皇聞?wù)的時(shí)候序无,一共有上圖中的5验毡、6、7三個(gè)步驟帝嗡,必須是三個(gè)步驟都執(zhí)行完畢才算是提交了事務(wù)晶通。那么在我們剛完成步驟5的時(shí)候,也就是redo log剛刷入磁盤文件的時(shí)候MySQL宕機(jī)了哟玷,此時(shí)怎么辦录择?這個(gè)因?yàn)闆]有最終的事務(wù)commit標(biāo)記在redo日志里,所以此次事務(wù)可以判定為不成功碗降,不會(huì)說redo日志文件里有這次更新的日志隘竭,但是binlog日志文件里沒有這次更新的日志,不會(huì)出現(xiàn)數(shù)據(jù)不一致的問題讼渊。
如果完成步驟6的時(shí)候此時(shí)MySQL宕機(jī)了动看,怎么辦?同理爪幻,因?yàn)闆]有redo log中的最終commit標(biāo)記菱皆,因此此時(shí)事務(wù)提交也是失敗的须误。
必須是redo log里有本次更新對(duì)應(yīng)的日志,binlog里也有本次更新對(duì)應(yīng)的日志仇轻,redo log和binlog完全是一致的京痢, 而且是在redo log中寫入了最終的事務(wù)commit標(biāo)記,此時(shí)事務(wù)才算提交成功篷店。
現(xiàn)在假設(shè)已經(jīng)提交事務(wù)了祭椰,此時(shí)一次更新“update users set name = 'xxx' where id=10”,它已經(jīng)把內(nèi)存里的buffer pool中的緩存數(shù)據(jù)更新了疲陕,同時(shí)磁盤里也存入了redo日志和binlog日志方淤,其中記錄了我們指定的把“id=10”這行數(shù)據(jù)修改了“name='xxx'”。但是這個(gè)時(shí)候磁盤上的數(shù)據(jù)文件里的“id=10”這行數(shù)據(jù)的name字段還是等于“zhangsan”這個(gè)舊值蹄殃。所以MySQL有一個(gè)携茂,會(huì)在之后某個(gè)時(shí)間里,隨機(jī)的把內(nèi)存buffer pool中的修改后的臟數(shù)據(jù)給刷回到磁盤上的數(shù)據(jù)文件里去诅岩,我們看下圖:
當(dāng)IO線程把buffer pool里的修改后的臟數(shù)據(jù)刷回磁盤之后讳苦,磁盤上的數(shù)據(jù)才會(huì)跟內(nèi)存里一樣。在IO線程把臟數(shù)據(jù)刷回磁盤之前吩谦,哪怕MySQL宕機(jī)也沒有關(guān)系鸳谜,因?yàn)橹貑⒅螅瑫?huì)根據(jù)redo日志恢復(fù)之前提交事務(wù)做過的修改到內(nèi)存里去逮京,然后等適當(dāng)時(shí)機(jī)卿堂,IO線程自然還是會(huì)把這個(gè)修改后的數(shù)據(jù)刷到磁盤上的數(shù)據(jù)文件里去束莫。
大家通過一次更新數(shù)據(jù)的流程,就可以清晰地看到InnoDB存儲(chǔ)引擎主要就是包含了一些buffer pool览绿、redo log buffer等內(nèi)存里的緩存數(shù)據(jù)策严,同時(shí)還包含了一些undo日志文件,redo日志文件等東西饿敲,同時(shí)MySQL server自己還有binlog日志文件妻导。在你執(zhí)行更新的時(shí)候,每條SQL語句都會(huì)對(duì)應(yīng)修改buffer pool里的緩存數(shù)據(jù)怀各、寫undo日志倔韭、寫redo log buffer幾個(gè)步驟。但是當(dāng)你提交事務(wù)的時(shí)候瓢对,一定會(huì)把redo log刷入磁盤寿酌,binlog刷入磁盤,完成redo log中的事務(wù)commit標(biāo)記硕蛹。最后醇疼,后臺(tái)的IO線程會(huì)隨機(jī)的把buffer pool里的臟數(shù)據(jù)輸入磁盤里去硕并。
1秧荆、為什么MySQL在更新數(shù)據(jù)的時(shí)候要大費(fèi)周章的搞這么多事情倔毙,包括buffer pool、redo log乙濒、undo log陕赃、binlog、事務(wù)提交琉兜、臟數(shù)據(jù)凯正。引入了一大堆的概念,有這么多復(fù)雜的流程和步驟豌蟋。
2廊散、為什么它反而最關(guān)鍵的修改磁盤里的數(shù)據(jù),要通過IO線程不定時(shí)的去執(zhí)行梧疲?
3允睹、為什么它不干脆直接就每次執(zhí)行SQL語句,直接就更新磁盤里的數(shù)據(jù)得了幌氮?