Mysql20—redo日志

redo日志是個(gè)啥

我們知道InnoDB存儲引擎是以頁為單位來管理存儲空間的穿稳,我們進(jìn)行的增刪改查操作其實(shí)本質(zhì)上都是在訪問頁面(包括讀頁面呼股、寫頁面阳啥、創(chuàng)建新頁面等操作)沸移。我們前邊嘮叨Buffer Pool的時(shí)候說過床牧,在真正訪問頁面之前荣回,需要把在磁盤上的頁緩存到內(nèi)存中的Buffer Pool之后才可以訪問。但是在嘮叨事務(wù)的時(shí)候又強(qiáng)調(diào)過一個(gè)稱之為持久性的特性戈咳,就是說對于一個(gè)已經(jīng)提交的事務(wù)心软,在事務(wù)提交后即使系統(tǒng)發(fā)生了崩潰壕吹,這個(gè)事務(wù)對數(shù)據(jù)庫中所做的更改也不能丟失。但是如果我們只在內(nèi)存的Buffer Pool中修改了頁面删铃,假設(shè)在事務(wù)提交后突然發(fā)生了某個(gè)故障耳贬,導(dǎo)致內(nèi)存中的數(shù)據(jù)都失效了,那么這個(gè)已經(jīng)提交了的事務(wù)對數(shù)據(jù)庫中所做的更改也就跟著丟失了猎唁,這是我們所不能忍受的(想想ATM機(jī)已經(jīng)提示狗哥轉(zhuǎn)賬成功咒劲,但之后由于服務(wù)器出現(xiàn)故障,重啟之后貓爺發(fā)現(xiàn)自己沒收到錢诫隅,貓爺就被砍死了)腐魂。那么如何保證這個(gè)持久性呢?一個(gè)很簡單的做法就是在事務(wù)提交完成之前把該事務(wù)所修改的所有頁面都刷新到磁盤逐纬,但是這個(gè)簡單粗暴的做法有些問題:

  • 刷新一個(gè)完整的數(shù)據(jù)頁太浪費(fèi)了

    有時(shí)候我們僅僅修改了某個(gè)頁面中的一個(gè)字節(jié)蛔屹,但是我們知道在InnoDB中是以頁為單位來進(jìn)行磁盤IO的,也就是說我們在該事務(wù)提交時(shí)不得不將一個(gè)完整的頁面從內(nèi)存中刷新到磁盤豁生,我們又知道一個(gè)頁面默認(rèn)是16KB大小兔毒,只修改一個(gè)字節(jié)就要刷新16KB的數(shù)據(jù)到磁盤上顯然是太浪費(fèi)了。

  • 隨機(jī)IO刷起來比較慢

    一個(gè)事務(wù)可能包含很多語句甸箱,即使是一條語句也可能修改許多頁面育叁,倒霉催的是該事務(wù)修改的這些頁面可能并不相鄰,這就意味著在將某個(gè)事務(wù)修改的Buffer Pool中的頁面刷新到磁盤時(shí)摇肌,需要進(jìn)行很多的隨機(jī)IO擂红,隨機(jī)IO比順序IO要慢,尤其對于傳統(tǒng)的機(jī)械硬盤來說围小。

咋辦呢昵骤?再次回到我們的初心:<span style="color:red">我們只是想讓已經(jīng)提交了的事務(wù)對數(shù)據(jù)庫中數(shù)據(jù)所做的修改永久生效,即使后來系統(tǒng)崩潰肯适,在重啟后也能把這種修改恢復(fù)出來</span>变秦。所以我們其實(shí)沒有必要在每次事務(wù)提交時(shí)就把該事務(wù)在內(nèi)存中修改過的全部頁面刷新到磁盤,只需要<span style="color:red">把修改了哪些東西記錄一下就好</span>框舔,比方說某個(gè)事務(wù)將系統(tǒng)表空間中的第100號頁面中偏移量為1000處的那個(gè)字節(jié)的值1改成2我們只需要記錄一下:

將第0號表空間的100號頁面的偏移量為1000處的值更新為2蹦玫。

這樣我們在事務(wù)提交時(shí),把上述內(nèi)容刷新到磁盤中刘绣,即使之后系統(tǒng)崩潰了樱溉,重啟之后只要按照上述內(nèi)容所記錄的步驟重新更新一下數(shù)據(jù)頁,那么該事務(wù)對數(shù)據(jù)庫中所做的修改又可以被恢復(fù)出來纬凤,也就意味著滿足持久性的要求福贞。因?yàn)樵谙到y(tǒng)奔潰重啟時(shí)需要按照上述內(nèi)容所記錄的步驟重新更新數(shù)據(jù)頁,所以上述內(nèi)容也被稱之為重做日志停士,英文名為redo log挖帘,我們也可以土洋結(jié)合完丽,稱之為redo日志。與在事務(wù)提交時(shí)將所有修改過的內(nèi)存中的頁面刷新到磁盤中相比拇舀,只將該事務(wù)執(zhí)行過程中產(chǎn)生的redo日志刷新到磁盤的好處如下:

  • redo日志占用的空間非常小

    存儲表空間ID逻族、頁號、偏移量以及需要更新的值所需的存儲空間是很小的骄崩,關(guān)于redo日志的格式我們稍后會詳細(xì)嘮叨聘鳞,現(xiàn)在只要知道一條redo日志占用的空間不是很大就好了。

  • redo日志是順序?qū)懭氪疟P的

    在執(zhí)行事務(wù)的過程中要拂,每執(zhí)行一條語句搁痛,就可能產(chǎn)生若干條redo日志,這些日志是按照產(chǎn)生的順序?qū)懭氪疟P的宇弛,也就是使用順序IO。

redo日志格式

通過上邊的內(nèi)容我們知道源请,redo日志本質(zhì)上只是記錄了一下事務(wù)對數(shù)據(jù)庫做了哪些修改枪芒。 設(shè)計(jì)InnoDB的大叔們針對事務(wù)對數(shù)據(jù)庫的不同修改場景定義了多種類型的redo日志,但是絕大部分類型的redo日志都有下邊這種通用的結(jié)構(gòu):

image.png

各個(gè)部分的詳細(xì)釋義如下:

  • type:該條redo日志的類型谁尸。

    MySQL 5.7.21這個(gè)版本中舅踪,設(shè)計(jì)InnoDB的大叔一共為redo日志設(shè)計(jì)了53種不同的類型,稍后會詳細(xì)介紹不同類型的redo日志良蛮。

  • space ID:表空間ID抽碌。

  • page number:頁號。

  • data:該條redo日志的具體內(nèi)容决瞳。

簡單的redo日志類型

我們前邊介紹InnoDB的記錄行格式的時(shí)候說過货徙,如果我們沒有為某個(gè)表顯式的定義主鍵,并且表中也沒有定義Unique鍵皮胡,那么InnoDB會自動(dòng)的為表添加一個(gè)稱之為row_id的隱藏列作為主鍵痴颊。為這個(gè)row_id隱藏列賦值的方式如下:

  • 服務(wù)器會在內(nèi)存中維護(hù)一個(gè)全局變量,每當(dāng)向某個(gè)包含隱藏的row_id列的表中插入一條記錄時(shí)屡贺,就會把該變量的值當(dāng)作新記錄的row_id列的值蠢棱,并且把該變量自增1。

  • 每當(dāng)這個(gè)變量的值為256的倍數(shù)時(shí)甩栈,就會將該變量的值刷新到系統(tǒng)表空間的頁號為7的頁面中一個(gè)稱之為Max Row ID的屬性處(我們前邊介紹表空間結(jié)構(gòu)時(shí)詳細(xì)說過)泻仙。

  • 當(dāng)系統(tǒng)啟動(dòng)時(shí),會將上邊提到的Max Row ID屬性加載到內(nèi)存中量没,將該值加上256之后賦值給我們前邊提到的全局變量(因?yàn)樵谏洗侮P(guān)機(jī)時(shí)該全局變量的值可能大于Max Row ID屬性值)玉转。

這個(gè)Max Row ID屬性占用的存儲空間是8個(gè)字節(jié),當(dāng)某個(gè)事務(wù)向某個(gè)包含row_id隱藏列的表插入一條記錄允蜈,并且為該記錄分配的row_id值為256的倍數(shù)時(shí)冤吨,就會向系統(tǒng)表空間頁號為7的頁面的相應(yīng)偏移量處寫入8個(gè)字節(jié)的值蒿柳。但是我們要知道,這個(gè)寫入實(shí)際上是在Buffer Pool中完成的漩蟆,我們需要為這個(gè)頁面的修改記錄一條redo日志垒探,以便在系統(tǒng)奔潰后能將已經(jīng)提交的該事務(wù)對該頁面所做的修改恢復(fù)出來。這種情況下對頁面的修改是極其簡單的怠李,redo日志中只需要記錄一下在某個(gè)頁面的某個(gè)偏移量處修改了幾個(gè)字節(jié)的值圾叼,具體被修改的內(nèi)容是啥就好了,設(shè)計(jì)InnoDB的大叔把這種極其簡單的redo日志稱之為物理日志捺癞,并且根據(jù)在頁面中寫入數(shù)據(jù)的多少劃分了幾種不同的redo日志類型:

  • MLOG_1BYTEtype字段對應(yīng)的十進(jìn)制數(shù)字為1):表示在頁面的某個(gè)偏移量處寫入1個(gè)字節(jié)的redo日志類型夷蚊。

  • MLOG_2BYTEtype字段對應(yīng)的十進(jìn)制數(shù)字為2):表示在頁面的某個(gè)偏移量處寫入2個(gè)字節(jié)的redo日志類型。

  • MLOG_4BYTEtype字段對應(yīng)的十進(jìn)制數(shù)字為4):表示在頁面的某個(gè)偏移量處寫入4個(gè)字節(jié)的redo日志類型髓介。

  • MLOG_8BYTEtype字段對應(yīng)的十進(jìn)制數(shù)字為8):表示在頁面的某個(gè)偏移量處寫入8個(gè)字節(jié)的redo日志類型惕鼓。

  • MLOG_WRITE_STRINGtype字段對應(yīng)的十進(jìn)制數(shù)字為30):表示在頁面的某個(gè)偏移量處寫入一串?dāng)?shù)據(jù)。

我們上邊提到的Max Row ID屬性實(shí)際占用8個(gè)字節(jié)的存儲空間唐础,所以在修改頁面中的該屬性時(shí)箱歧,會記錄一條類型為MLOG_8BYTE的redo日志,MLOG_8BYTE的redo日志結(jié)構(gòu)如下所示:

image.png

其余MLOG_1BYTE一膨、MLOG_2BYTE呀邢、MLOG_4BYTE類型的redo日志結(jié)構(gòu)和MLOG_8BYTE的類似,只不過具體數(shù)據(jù)中包含對應(yīng)個(gè)字節(jié)的數(shù)據(jù)罷了豹绪。MLOG_WRITE_STRING類型的redo日志表示寫入一串?dāng)?shù)據(jù)价淌,但是因?yàn)椴荒艽_定寫入的具體數(shù)據(jù)占用多少字節(jié),所以需要在日志結(jié)構(gòu)中添加一個(gè)len字段:

image.png

只要將MLOG_WRITE_STRING類型的redo日志的len字段填充上1瞒津、2蝉衣、4、8這些數(shù)字仲智,就可以分別替代MLOG_1BYTE买乃、MLOG_2BYTE、MLOG_4BYTE钓辆、MLOG_8BYTE這些類型的redo日志剪验,為啥還要多此一舉設(shè)計(jì)這么多類型呢?還不是因?yàn)槭】臻g啊前联,能不寫len字段就不寫len字段功戚,省一個(gè)字節(jié)算一個(gè)字節(jié)。

復(fù)雜一些的redo日志類型

有時(shí)候執(zhí)行一條語句會修改非常多的頁面似嗤,包括系統(tǒng)數(shù)據(jù)頁面和用戶數(shù)據(jù)頁面(用戶數(shù)據(jù)指的就是聚簇索引和二級索引對應(yīng)的B+樹)啸臀。以一條INSERT語句為例,它除了要向B+樹的頁面中插入數(shù)據(jù),也可能更新系統(tǒng)數(shù)據(jù)Max Row ID的值乘粒,不過對于我們用戶來說豌注,平時(shí)更關(guān)心的是語句對B+樹所做更新:

  • 表中包含多少個(gè)索引,一條INSERT語句就可能更新多少棵B+樹灯萍。

  • 針對某一棵B+樹來說轧铁,既可能更新葉子節(jié)點(diǎn)頁面,也可能更新內(nèi)節(jié)點(diǎn)頁面旦棉,也可能創(chuàng)建新的頁面(在該記錄插入的葉子節(jié)點(diǎn)的剩余空間比較少齿风,不足以存放該記錄時(shí),會進(jìn)行頁面的分裂绑洛,在內(nèi)節(jié)點(diǎn)頁面中添加目錄項(xiàng)記錄)救斑。

在語句執(zhí)行過程中,INSERT語句對所有頁面的修改都得保存到redo日志中去真屯。這句話說的比較輕巧脸候,做起來可就比較麻煩了,比方說將記錄插入到聚簇索引中時(shí)绑蔫,如果定位到的葉子節(jié)點(diǎn)的剩余空間足夠存儲該記錄時(shí)纪他,那么只更新該葉子節(jié)點(diǎn)頁面就好晾匠,那么只記錄一條MLOG_WRITE_STRING類型的redo日志,表明在頁面的某個(gè)偏移量處增加了哪些數(shù)據(jù)就好了么梯刚?那就too young too naive了~ 別忘了一個(gè)數(shù)據(jù)頁中除了存儲實(shí)際的記錄之后凉馆,還有什么File Header、Page Header亡资、Page Directory等等部分(在嘮叨數(shù)據(jù)頁的章節(jié)有詳細(xì)講解)澜共,所以每往葉子節(jié)點(diǎn)代表的數(shù)據(jù)頁里插入一條記錄時(shí),還有其他很多地方會跟著更新锥腻,比如說:

  • 可能更新Page Directory中的槽信息嗦董。

  • Page Header中的各種頁面統(tǒng)計(jì)信息,比如PAGE_N_DIR_SLOTS表示的槽數(shù)量可能會更改瘦黑,PAGE_HEAP_TOP代表的還未使用的空間最小地址可能會更改京革,PAGE_N_HEAP代表的本頁面中的記錄數(shù)量可能會更改,吧啦吧啦幸斥,各種信息都可能會被修改匹摇。

  • 我們知道在數(shù)據(jù)頁里的記錄是按照索引列從小到大的順序組成一個(gè)單向鏈表的,每插入一條記錄甲葬,還需要更新上一條記錄的記錄頭信息中的next_record屬性來維護(hù)這個(gè)單向鏈表廊勃。

  • 還有別的吧啦吧啦的更新的地方,就不一一嘮叨了...

畫一個(gè)簡易的示意圖就像是這樣:


image.png

說了這么多经窖,就是想表達(dá):把一條記錄插入到一個(gè)頁面時(shí)需要更改的地方非常多坡垫。這時(shí)我們?nèi)绻褂蒙线吔榻B的簡單的物理redo日志來記錄這些修改時(shí)梭灿,可以有兩種解決方案:

  • 方案一:在每個(gè)修改的地方都記錄一條redo日志。

    也就是如上圖所示冰悠,有多少個(gè)加粗的塊堡妒,就寫多少條物理redo日志。這樣子記錄redo日志的缺點(diǎn)是顯而易見的屿脐,因?yàn)楸恍薷牡牡胤绞窃谔嗔颂樵椋赡苡涗浀?code>redo日志占用的空間都比整個(gè)頁面占用的空間都多了~

  • 方案二:將整個(gè)頁面的第一個(gè)被修改的字節(jié)到最后一個(gè)修改的字節(jié)之間所有的數(shù)據(jù)當(dāng)成是一條物理redo日志中的具體數(shù)據(jù)。

從圖中也可以看出來的诵,第一個(gè)被修改的字節(jié)到最后一個(gè)修改的字節(jié)之間仍然有許多沒有修改過的數(shù)據(jù)万栅,我們把這些沒有修改的數(shù)據(jù)也加入到redo日志中去豈不是太浪費(fèi)了~

正因?yàn)樯鲜鰞煞N使用物理redo日志的方式來記錄某個(gè)頁面中做了哪些修改比較浪費(fèi),設(shè)計(jì)InnoDB的大叔本著勤儉節(jié)約的初心西疤,提出了一些新的redo日志類型烦粒,比如:

  • MLOG_REC_INSERT(對應(yīng)的十進(jìn)制數(shù)字為9):表示插入一條使用非緊湊行格式的記錄時(shí)的redo日志類型。

  • MLOG_COMP_REC_INSERT(對應(yīng)的十進(jìn)制數(shù)字為38):表示插入一條使用緊湊行格式的記錄時(shí)的redo日志類型代赁。

小貼士:

Redundant是一種比較原始的行格式扰她,它就是非緊湊的。而Compact芭碍、Dynamic以及Compressed行格式是較新的行格式徒役,它們是緊湊的(占用更小的存儲空間)。
  • MLOG_COMP_PAGE_CREATEtype字段對應(yīng)的十進(jìn)制數(shù)字為58):表示創(chuàng)建一個(gè)存儲緊湊行格式記錄的頁面的redo日志類型窖壕。

  • MLOG_COMP_REC_DELETEtype字段對應(yīng)的十進(jìn)制數(shù)字為42):表示刪除一條使用緊湊行格式記錄的redo日志類型忧勿。

  • MLOG_COMP_LIST_START_DELETEtype字段對應(yīng)的十進(jìn)制數(shù)字為44):表示從某條給定記錄開始刪除頁面中的一系列使用緊湊行格式記錄的redo日志類型。

  • MLOG_COMP_LIST_END_DELETEtype字段對應(yīng)的十進(jìn)制數(shù)字為43):與MLOG_COMP_LIST_START_DELETE類型的redo日志呼應(yīng)瞻讽,表示刪除一系列記錄直到MLOG_COMP_LIST_END_DELETE類型的redo日志對應(yīng)的記錄為止鸳吸。

小貼士:

我們前邊嘮叨InnoDB數(shù)據(jù)頁格式的時(shí)候重點(diǎn)強(qiáng)調(diào)過,數(shù)據(jù)頁中的記錄是按照索引列大小的順序組成單向鏈表的速勇。有時(shí)候我們會有刪除索引列的值在某個(gè)區(qū)間范圍內(nèi)的所有記錄的需求晌砾,這時(shí)候如果我們每刪除一條記錄就寫一條redo日志的話,效率可能有點(diǎn)低烦磁,所以提出MLOG_COMP_LIST_START_DELETE和MLOG_COMP_LIST_END_DELETE類型的redo日志养匈,可以很大程度上減少redo日志的條數(shù)。
  • MLOG_ZIP_PAGE_COMPRESStype字段對應(yīng)的十進(jìn)制數(shù)字為51):表示壓縮一個(gè)數(shù)據(jù)頁的redo日志類型都伪。

  • ······還有很多很多種類型乖寒,這就不列舉了,等用到再說哈~

這些類型的redo日志既包含物理層面的意思院溺,也包含邏輯層面的意思楣嘁,具體指:

  • 物理層面看,這些日志都指明了對哪個(gè)表空間的哪個(gè)頁進(jìn)行了修改。

  • 邏輯層面看逐虚,在系統(tǒng)崩潰重啟時(shí)聋溜,并不能直接根據(jù)這些日志里的記載,將頁面內(nèi)的某個(gè)偏移量處恢復(fù)成某個(gè)數(shù)據(jù)叭爱,而是需要調(diào)用一些事先準(zhǔn)備好的函數(shù)撮躁,執(zhí)行完這些函數(shù)后才可以將頁面恢復(fù)成系統(tǒng)奔潰前的樣子。

大家看到這可能有些懵逼买雾,我們還是以類型為MLOG_COMP_REC_INSERT這個(gè)代表插入一條使用緊湊行格式的記錄時(shí)的redo日志為例來理解一下我們上邊所說的物理層面和邏輯層面到底是個(gè)啥意思把曼。廢話少說,直接看一下這個(gè)類型為MLOG_COMP_REC_INSERT的redo日志的結(jié)構(gòu)(由于字段太多了漓穿,我們把它們豎著看效果好些):

image.png

這個(gè)類型為MLOG_COMP_REC_INSERTredo日志結(jié)構(gòu)有幾個(gè)地方需要大家注意:

  • 我們前邊在嘮叨索引的時(shí)候說過嗤军,在一個(gè)數(shù)據(jù)頁里,不論是葉子節(jié)點(diǎn)還是非葉子節(jié)點(diǎn)晃危,記錄都是按照索引列從小到大的順序排序的叙赚。對于二級索引來說,當(dāng)索引列的值相同時(shí)僚饭,記錄還需要按照主鍵值進(jìn)行排序震叮。圖中n_uniques的值的含義是在一條記錄中,需要幾個(gè)字段的值才能確保記錄的唯一性鳍鸵,這樣當(dāng)插入一條記錄時(shí)就可以按照記錄的前n_uniques個(gè)字段進(jìn)行排序苇瓣。對于聚簇索引來說,n_uniques的值為主鍵的列數(shù)偿乖,對于其他二級索引來說钓简,該值為索引列數(shù)+主鍵列數(shù)。這里需要注意的是汹想,唯一二級索引的值可能為NULL,所以該值仍然為索引列數(shù)+主鍵列數(shù)撤蚊。

  • field1_len ~ fieldn_len代表著該記錄若干個(gè)字段占用存儲空間的大小古掏,需要注意的是,這里不管該字段的類型是固定長度大小的(比如INT)侦啸,還是可變長度大胁弁佟(比如VARCHAR(M))的,該字段占用的大小始終要寫入redo日志中光涂。

  • offset代表的是該記錄的前一條記錄在頁面中的地址庞萍。為啥要記錄前一條記錄的地址呢廷痘?這是因?yàn)槊肯驍?shù)據(jù)頁插入一條記錄玖媚,都需要修改該頁面中維護(hù)的記錄鏈表,每條記錄的記錄頭信息中都包含一個(gè)稱為next_record的屬性瘫絮,所以在插入新記錄時(shí),需要修改前一條記錄的next_record屬性私恬。

  • 我們知道一條記錄其實(shí)由額外信息真實(shí)數(shù)據(jù)這兩部分組成债沮,這兩個(gè)部分的總大小就是一條記錄占用存儲空間的總大小。通過end_seg_len的值可以間接的計(jì)算出一條記錄占用存儲空間的總大小本鸣,為啥不直接存儲一條記錄占用存儲空間的總大小呢疫衩?這是因?yàn)閷?code>redo日志是一個(gè)非常頻繁的操作,設(shè)計(jì)InnoDB的大叔想方設(shè)法想減小redo日志本身占用的存儲空間大小荣德,所以想了一些彎彎繞的算法來實(shí)現(xiàn)這個(gè)目標(biāo)闷煤,end_seg_len這個(gè)字段就是為了節(jié)省redo日志存儲空間而提出來的。至于具體設(shè)計(jì)InnoDB的大叔到底是用了什么神奇魔法減小redo日志大小的涮瞻,我們這就不多嘮叨了鲤拿,因?yàn)榈拇_有那么一丟丟小復(fù)雜,說清楚還是有一點(diǎn)點(diǎn)麻煩的饲宛,而且說明白了也沒啥用皆愉。

  • mismatch_index的值也是為了節(jié)省redo日志的大小而設(shè)立的,大家可以忽略艇抠。

很顯然這個(gè)類型為MLOG_COMP_REC_INSERT的redo日志并沒有記錄PAGE_N_DIR_SLOTS的值修改為了啥幕庐,PAGE_HEAP_TOP的值修改為了啥,PAGE_N_HEAP的值修改為了啥等等這些信息家淤,而只是把在本頁面中插入一條記錄所有必備的要素記了下來异剥,之后系統(tǒng)奔潰重啟時(shí),服務(wù)器會調(diào)用相關(guān)向某個(gè)頁面插入一條記錄的那個(gè)函數(shù)絮重,而redo日志中的那些數(shù)據(jù)就可以被當(dāng)成是調(diào)用這個(gè)函數(shù)所需的參數(shù)冤寿,在調(diào)用完該函數(shù)后,頁面中的PAGE_N_DIR_SLOTS青伤、PAGE_HEAP_TOP督怜、PAGE_N_HEAP等等的值也就都被恢復(fù)到系統(tǒng)奔潰前的樣子了。這就是所謂的邏輯日志的意思狠角。

redo日志格式小結(jié)

雖然上邊說了一大堆關(guān)于redo日志格式的內(nèi)容号杠,但是如果你不是為了寫一個(gè)解析redo日志的工具或者自己開發(fā)一套redo日志系統(tǒng)的話,那就沒必要把InnoDB中的各種類型的redo日志格式都研究的透透的丰歌,沒那個(gè)必要姨蟋。上邊我只是象征性的介紹了幾種類型的redo日志格式,目的還是想讓大家明白:redo日志會把事務(wù)在執(zhí)行過程中對數(shù)據(jù)庫所做的所有修改都記錄下來立帖,在之后系統(tǒng)奔潰重啟后可以把事務(wù)所做的任何修改都恢復(fù)出來眼溶。

為了節(jié)省redo日志占用的存儲空間大小,設(shè)計(jì)InnoDB的大叔對redo日志中的某些數(shù)據(jù)還可能進(jìn)行壓縮處理晓勇,比方說spacd ID和page number一般占用4個(gè)字節(jié)來存儲堂飞,但是經(jīng)過壓縮后灌旧,可能使用更小的空間來存儲。具體壓縮算法就不嘮叨了酝静。

Mini-Transaction

以組的形式寫入redo日志

語句在執(zhí)行過程中可能修改若干個(gè)頁面节榜。比如我們前邊說的一條INSERT語句可能修改系統(tǒng)表空間頁號為7的頁面的Max Row ID屬性(當(dāng)然也可能更新別的系統(tǒng)頁面,只不過我們沒有都列舉出來而已)别智,還會更新聚簇索引和二級索引對應(yīng)B+樹中的頁面宗苍。由于對這些頁面的更改都發(fā)生在Buffer Pool中,所以在修改完頁面之后薄榛,需要記錄一下相應(yīng)的redo日志讳窟。在執(zhí)行語句的過程中產(chǎn)生的redo日志被設(shè)計(jì)InnoDB的大叔人為的劃分成了若干個(gè)不可分割的組,比如:

  • 更新Max Row ID屬性時(shí)產(chǎn)生的redo日志是不可分割的敞恋。

  • 向聚簇索引對應(yīng)B+樹的頁面中插入一條記錄時(shí)產(chǎn)生的redo日志是不可分割的丽啡。

  • 向某個(gè)二級索引對應(yīng)B+樹的頁面中插入一條記錄時(shí)產(chǎn)生的redo日志是不可分割的。

  • 還有其他的一些對頁面的訪問操作時(shí)產(chǎn)生的redo日志是不可分割的硬猫。补箍。。

怎么理解這個(gè)不可分割的意思呢啸蜜?我們以向某個(gè)索引對應(yīng)的B+樹插入一條記錄為例坑雅,在向B+樹中插入這條記錄之前,需要先定位到這條記錄應(yīng)該被插入到哪個(gè)葉子節(jié)點(diǎn)代表的數(shù)據(jù)頁中衬横,定位到具體的數(shù)據(jù)頁之后裹粤,有兩種可能的情況:

  • 情況一:該數(shù)據(jù)頁的剩余的空閑空間充足,足夠容納這一條待插入記錄蜂林,那么事情很簡單遥诉,直接把記錄插入到這個(gè)數(shù)據(jù)頁中,記錄一條類型為MLOG_COMP_REC_INSERTredo日志就好了噪叙,我們把這種情況稱之為樂觀插入矮锈。假如某個(gè)索引對應(yīng)的B+樹長這樣:
image.png

現(xiàn)在我們要插入一條鍵值為10的記錄,很顯然需要被插入到頁b中睁蕾,由于頁b現(xiàn)在有足夠的空間容納一條記錄苞笨,所以直接將該記錄插入到頁b中就好了,就像這樣:

image.png
  • 情況二:該數(shù)據(jù)頁剩余的空閑空間不足惫霸,那么事情就悲劇了,我們前邊說過葱弟,遇到這種情況要進(jìn)行所謂的頁分裂操作壹店,也就是新建一個(gè)葉子節(jié)點(diǎn),然后把原先數(shù)據(jù)頁中的一部分記錄復(fù)制到這個(gè)新的數(shù)據(jù)頁中芝加,然后再把記錄插入進(jìn)去硅卢,把這個(gè)葉子節(jié)點(diǎn)插入到葉子節(jié)點(diǎn)鏈表中射窒,最后還要在內(nèi)節(jié)點(diǎn)中添加一條目錄項(xiàng)記錄指向這個(gè)新創(chuàng)建的頁面。很顯然将塑,這個(gè)過程要對多個(gè)頁面進(jìn)行修改脉顿,也就意味著會產(chǎn)生多條redo日志,我們把這種情況稱之為悲觀插入点寥。假如某個(gè)索引對應(yīng)的B+樹長這樣:
image.png

現(xiàn)在我們要插入一條鍵值為10的記錄艾疟,很顯然需要被插入到頁b中,但是從圖中也可以看出來敢辩,此時(shí)頁b已經(jīng)塞滿了記錄蔽莱,沒有更多的空閑空間來容納這條新記錄了,所以我們需要進(jìn)行頁面的分裂操作戚长,就像這樣:

image.png

如果作為內(nèi)節(jié)點(diǎn)的頁a的剩余空閑空間也不足以容納增加一條目錄項(xiàng)記錄盗冷,那需要繼續(xù)做內(nèi)節(jié)點(diǎn)頁a的分裂操作,也就意味著會修改更多的頁面同廉,從而產(chǎn)生更多的redo日志仪糖。另外,對于悲觀插入來說迫肖,由于需要新申請數(shù)據(jù)頁锅劝,還需要改動(dòng)一些系統(tǒng)頁面,比方說要修改各種段咒程、區(qū)的統(tǒng)計(jì)信息信息鸠天,各種鏈表的統(tǒng)計(jì)信息(比如什么FREE鏈表、FSP_FREE_FRAG鏈表吧啦吧啦我們在嘮叨表空間那一章中介紹過的各種東東)等等等等帐姻,反正總共需要記錄的redo日志有二稠集、三十條。

設(shè)計(jì)InnoDB的大叔們認(rèn)為向某個(gè)索引對應(yīng)的B+樹中插入一條記錄的這個(gè)過程必須是原子的饥瓷,不能說插了一半之后就停止了剥纷。比方說在悲觀插入過程中,新的頁面已經(jīng)分配好了呢铆,數(shù)據(jù)也復(fù)制過去了晦鞋,新的記錄也插入到頁面中了,可是沒有向內(nèi)節(jié)點(diǎn)中插入一條目錄項(xiàng)記錄棺克,這個(gè)插入過程就是不完整的悠垛,這樣會形成一棵不正確的B+樹。我們知道redo日志是為了在系統(tǒng)奔潰重啟時(shí)恢復(fù)崩潰前的狀態(tài)娜谊,如果在悲觀插入的過程中只記錄了一部分redo日志确买,那么在系統(tǒng)奔潰重啟時(shí)會將索引對應(yīng)的B+樹恢復(fù)成一種不正確的狀態(tài),這是設(shè)計(jì)InnoDB的大叔們所不能忍受的纱皆。所以他們規(guī)定在執(zhí)行這些需要保證原子性的操作時(shí)必須以組的形式來記錄的redo日志湾趾,在進(jìn)行系統(tǒng)奔潰重啟恢復(fù)時(shí)芭商,針對某個(gè)組中的redo日志,要么把全部的日志都恢復(fù)掉搀缠,要么一條也不恢復(fù)铛楣。怎么做到的呢?這得分情況討論:

  • 有的需要保證原子性的操作會生成多條redo日志艺普,比如向某個(gè)索引對應(yīng)的B+樹中進(jìn)行一次悲觀插入就需要生成許多條redo日志簸州。

    如何把這些redo日志劃分到一個(gè)組里邊兒呢?設(shè)計(jì)InnoDB的大叔做了一個(gè)很簡單的小把戲衷敌,就是在該組中的最后一條redo日志后邊加上一條特殊類型的redo日志勿侯,該類型名稱為MLOG_MULTI_REC_ENDtype字段對應(yīng)的十進(jìn)制數(shù)字為31缴罗,該類型的redo日志結(jié)構(gòu)很簡單助琐,只有一個(gè)type字段:

image.png

所以某個(gè)需要保證原子性的操作產(chǎn)生的一系列redo日志必須要以一個(gè)類型為MLOG_MULTI_REC_END結(jié)尾,就像這樣:

image.png

這樣在系統(tǒng)奔潰重啟進(jìn)行恢復(fù)時(shí)面氓,只有當(dāng)解析到類型為MLOG_MULTI_REC_END的redo日志兵钮,才認(rèn)為解析到了一組完整的redo日志,才會進(jìn)行恢復(fù)舌界。否則的話直接放棄前邊解析到的redo日志掘譬。

  • 有的需要保證原子性的操作只生成一條redo日志,比如更新Max Row ID屬性的操作就只會生成一條redo日志呻拌。

    其實(shí)在一條日志后邊跟一個(gè)類型為MLOG_MULTI_REC_ENDredo日志也是可以的葱轩,不過設(shè)計(jì)InnoDB的大叔比較勤儉節(jié)約,他們不想浪費(fèi)一個(gè)比特位藐握。別忘了雖然redo日志的類型比較多靴拱,但撐死了也就是幾十種,是小于127這個(gè)數(shù)字的猾普,也就是說我們用7個(gè)比特位就足以包括所有的redo日志類型袜炕,而type字段其實(shí)是占用1個(gè)字節(jié)的,也就是說我們可以省出來一個(gè)比特位用來表示該需要保證原子性的操作只產(chǎn)生單一的一條redo日志初家,示意圖如下:

image.png

如果type字段的第一個(gè)比特位為1偎窘,代表該需要保證原子性的操作只產(chǎn)生了單一的一條redo日志,否則表示該需要保證原子性的操作產(chǎn)生了一系列的redo日志溜在。

Mini-Transaction的概念

設(shè)計(jì)MySQL的大叔把對底層頁面中的一次原子訪問的過程稱之為一個(gè)Mini-Transaction陌知,簡稱mtr,比如上邊所說的修改一次Max Row ID的值算是一個(gè)Mini-Transaction掖肋,向某個(gè)索引對應(yīng)的B+樹中插入一條記錄的過程也算是一個(gè)Mini-Transaction仆葡。通過上邊的敘述我們也知道,一個(gè)所謂的mtr可以包含一組redo日志培遵,在進(jìn)行奔潰恢復(fù)時(shí)這一組redo日志作為一個(gè)不可分割的整體浙芙。

一個(gè)事務(wù)可以包含若干條語句,每一條語句其實(shí)是由若干個(gè)mtr組成籽腕,每一個(gè)mtr又可以包含若干條redo日志嗡呼,畫個(gè)圖表示它們的關(guān)系就是這樣:

image.png

redo日志的寫入過程

redo log block

設(shè)計(jì)InnoDB的大叔為了更好的進(jìn)行系統(tǒng)奔潰恢復(fù),他們把通過mtr生成的redo日志都放在了大小為512字節(jié)的頁中皇耗。為了和我們前邊提到的表空間中的頁做區(qū)別南窗,我們這里把用來存儲redo日志的頁稱為block(你心里清楚頁和block的意思其實(shí)差不多就行了)。一個(gè)redo log block的示意圖如下:

image.png

真正的redo日志都是存儲到占用496字節(jié)大小的log block body中郎楼,圖中的log block header和log block trailer存儲的是一些管理信息万伤。我們來看看這些所謂的管理信息都是啥:

image.png

其中l(wèi)og block header的幾個(gè)屬性的意思分別如下:

  • LOG_BLOCK_HDR_NO:每一個(gè)block都有一個(gè)大于0的唯一標(biāo)號,本屬性就表示該標(biāo)號值呜袁。

  • LOG_BLOCK_HDR_DATA_LEN:表示block中已經(jīng)使用了多少字節(jié)敌买,初始值為12(因?yàn)?code>log block body從第12個(gè)字節(jié)處開始)。隨著往block中寫入的redo日志越來也多阶界,本屬性值也跟著增長虹钮。如果log block body已經(jīng)被全部寫滿,那么本屬性的值被設(shè)置為512膘融。

  • LOG_BLOCK_FIRST_REC_GROUP:一條redo日志也可以稱之為一條redo日志記錄(redo log record)芙粱,一個(gè)mtr會生產(chǎn)多條redo日志記錄,這些redo日志記錄被稱之為一個(gè)redo日志記錄組(redo log record group)氧映。LOG_BLOCK_FIRST_REC_GROUP就代表該block中第一個(gè)mtr生成的redo日志記錄組的偏移量(其實(shí)也就是這個(gè)block里第一個(gè)mtr生成的第一條redo日志的偏移量)春畔。

  • LOG_BLOCK_CHECKPOINT_NO:表示所謂的checkpoint的序號,checkpoint是我們后續(xù)內(nèi)容的重點(diǎn)岛都,現(xiàn)在先不用清楚它的意思律姨,稍安勿躁。

log block trailer中屬性的意思如下:

  • LOG_BLOCK_CHECKSUM:表示block的校驗(yàn)值疗绣,用于正確性校驗(yàn)线召,我們暫時(shí)不關(guān)心它。

redo日志緩沖區(qū)

我們前邊說過多矮,設(shè)計(jì)InnoDB的大叔為了解決磁盤速度過慢的問題而引入了Buffer Pool缓淹。同理,寫入redo日志時(shí)也不能直接直接寫到磁盤上塔逃,實(shí)際上在服務(wù)器啟動(dòng)時(shí)就向操作系統(tǒng)申請了一大片稱之為redo log buffer的連續(xù)內(nèi)存空間讯壶,翻譯成中文就是redo日志緩沖區(qū),我們也可以簡稱為log buffer湾盗。這片內(nèi)存空間被劃分成若干個(gè)連續(xù)的redo log block伏蚊,就像這樣:

image.png

我們可以通過啟動(dòng)參數(shù)innodb_log_buffer_size來指定log buffer的大小,在MySQL 5.7.21這個(gè)版本中格粪,該啟動(dòng)參數(shù)的默認(rèn)值為16MB躏吊。

redo日志寫入log buffer

向log buffer中寫入redo日志的過程是順序的氛改,也就是先往前邊的block中寫,當(dāng)該block的空閑空間用完之后再往下一個(gè)block中寫比伏。當(dāng)我們想往log buffer中寫入redo日志時(shí)胜卤,第一個(gè)遇到的問題就是應(yīng)該寫在哪個(gè)block的哪個(gè)偏移量處,所以設(shè)計(jì)InnoDB的大叔特意提供了一個(gè)稱之為buf_free的全局變量赁项,該變量指明后續(xù)寫入的redo日志應(yīng)該寫入到log buffer中的哪個(gè)位置葛躏,如圖所示:

image.png

我們前邊說過一個(gè)mtr執(zhí)行過程中可能產(chǎn)生若干條redo日志,這些redo日志是一個(gè)不可分割的組悠菜,所以其實(shí)并不是每生成一條redo日志舰攒,就將其插入到log buffer中,而是每個(gè)mtr運(yùn)行過程中產(chǎn)生的日志先暫時(shí)存到一個(gè)地方悔醋,當(dāng)該mtr結(jié)束的時(shí)候摩窃,將過程中產(chǎn)生的一組redo日志再全部復(fù)制到log buffer中。我們現(xiàn)在假設(shè)有兩個(gè)名為T1芬骄、T2的事務(wù)偶芍,每個(gè)事務(wù)都包含2個(gè)mtr,我們給這幾個(gè)mtr命名一下:

  • 事務(wù)T1的兩個(gè)mtr分別稱為mtr_T1_1mtr_T1_2德玫。

  • 事務(wù)T2的兩個(gè)mtr分別稱為mtr_T2_1mtr_T2_2匪蟀。

每個(gè)mtr都會產(chǎn)生一組redo日志,用示意圖來描述一下這些mtr產(chǎn)生的日志情況:

image.png

不同的事務(wù)可能是并發(fā)執(zhí)行的宰僧,所以T1材彪、T2之間的mtr可能是交替執(zhí)行的。每當(dāng)一個(gè)mtr執(zhí)行完成時(shí)琴儿,伴隨該mtr生成的一組redo日志就需要被復(fù)制到log buffer中段化,也就是說不同事務(wù)的mtr可能是交替寫入log buffer的,我們畫個(gè)示意圖(為了美觀造成,我們把一個(gè)mtr中產(chǎn)生的所有的redo日志當(dāng)作一個(gè)整體來畫):

image.png

redo日志刷盤時(shí)機(jī)

我們前邊說mtr運(yùn)行過程中產(chǎn)生的一組redo日志在mtr結(jié)束時(shí)會被復(fù)制到log buffer中显熏,可是這些日志總在內(nèi)存里呆著也不是個(gè)辦法,在一些情況下它們會被刷新到磁盤里晒屎,比如:

  • log buffer空間不足時(shí)

    log buffer的大小是有限的(通過系統(tǒng)變量innodb_log_buffer_size指定)喘蟆,如果不停的往這個(gè)有限大小的log buffer里塞入日志,很快它就會被填滿鼓鲁。設(shè)計(jì)InnoDB的大叔認(rèn)為如果當(dāng)前寫入log bufferredo日志量已經(jīng)占滿了log buffer總?cè)萘康拇蠹s一半左右蕴轨,就需要把這些日志刷新到磁盤上。

  • 事務(wù)提交時(shí)

    我們前邊說過之所以使用redo日志主要是因?yàn)樗加玫目臻g少骇吭,還是順序?qū)懗热酰谑聞?wù)提交時(shí)可以不把修改過的Buffer Pool頁面刷新到磁盤,但是為了保證持久性,必須要把修改這些頁面對應(yīng)的redo日志刷新到磁盤棘脐。

  • 后臺線程不停的刷刷刷

    后臺有一個(gè)線程斜筐,大約每秒都會刷新一次log buffer中的redo日志到磁盤。

  • 正常關(guān)閉服務(wù)器時(shí)

  • 做所謂的checkpoint時(shí)(我們現(xiàn)在沒介紹過checkpoint的概念蛀缝,稍后會仔細(xì)嘮叨奴艾,稍安勿躁)

  • 其他的一些情況...

redo日志文件組

MySQL的數(shù)據(jù)目錄(使用SHOW VARIABLES LIKE 'datadir'查看)下默認(rèn)有兩個(gè)名為ib_logfile0和ib_logfile1的文件,log buffer中的日志默認(rèn)情況下就是刷新到這兩個(gè)磁盤文件中内斯。如果我們對默認(rèn)的redo日志文件不滿意,可以通過下邊幾個(gè)啟動(dòng)參數(shù)來調(diào)節(jié):

  • innodb_log_group_home_dir

    該參數(shù)指定了redo日志文件所在的目錄像啼,默認(rèn)值就是當(dāng)前的數(shù)據(jù)目錄俘闯。

  • innodb_log_file_size

    該參數(shù)指定了每個(gè)redo日志文件的大小,在MySQL 5.7.21這個(gè)版本中的默認(rèn)值為48MB忽冻,

  • innodb_log_files_in_group

    該參數(shù)指定redo日志文件的個(gè)數(shù)真朗,默認(rèn)值為2,最大值為100僧诚。

從上邊的描述中可以看到遮婶,磁盤上的redo日志文件不只一個(gè),而是以一個(gè)日志文件組的形式出現(xiàn)的湖笨。這些文件以ib_logfile[數(shù)字](數(shù)字可以是0旗扑、1、2...)的形式進(jìn)行命名慈省。在將redo日志寫入日志文件組時(shí)臀防,是從ib_logfile0開始寫,如果ib_logfile0寫滿了边败,就接著ib_logfile1寫袱衷,同理,ib_logfile1寫滿了就去寫ib_logfile2笑窜,依此類推致燥。如果寫到最后一個(gè)文件該咋辦?那就重新轉(zhuǎn)到ib_logfile0繼續(xù)寫排截,所以整個(gè)過程如下圖所示:

image.png

總共的redo日志文件大小其實(shí)就是:innodb_log_file_size × innodb_log_files_in_group嫌蚤。

小貼士:如果采用循環(huán)使用的方式向redo日志文件組里寫數(shù)據(jù)的話,那豈不是要追尾断傲,也就是后寫入的redo日志覆蓋掉前邊寫的redo日志搬葬?當(dāng)然可能了!所以設(shè)計(jì)InnoDB的大叔提出了checkpoint的概念艳悔,稍后我們重點(diǎn)嘮叨~

redo日志文件格式

我們前邊說過log buffer本質(zhì)上是一片連續(xù)的內(nèi)存空間急凰,被劃分成了若干個(gè)512字節(jié)大小的block。將log buffer中的redo日志刷新到磁盤的本質(zhì)就是把block的鏡像寫入日志文件中,所以redo日志文件其實(shí)也是由若干個(gè)512字節(jié)大小的block組成抡锈。

redo日志文件組中的每個(gè)文件大小都一樣疾忍,格式也一樣,都是由兩部分組成:

  • 前2048個(gè)字節(jié)床三,也就是前4個(gè)block是用來存儲一些管理信息的一罩。
  • 從第2048字節(jié)往后是用來存儲log buffer中的block鏡像的。

所以我們前邊所說的循環(huán)使用redo日志文件撇簿,其實(shí)是從每個(gè)日志文件的第2048個(gè)字節(jié)開始算聂渊,畫個(gè)示意圖就是這樣:

image.png

普通block的格式我們在嘮叨log buffer的時(shí)候都說過了,就是log block header四瘫、log block body汉嗽、log block trialer這三個(gè)部分,就不重復(fù)介紹了找蜜。這里需要介紹一下每個(gè)redo日志文件前2048個(gè)字節(jié)饼暑,也就是前4個(gè)特殊block的格式都是干嘛的,廢話少說洗做,先看圖:

image.png

從圖中可以看出來弓叛,這4個(gè)block分別是:

  • log file header:描述該redo日志文件的一些整體屬性,看一下它的結(jié)構(gòu):
image.png

各個(gè)屬性的具體釋義如下:

屬性名 長度(單位:字節(jié)) 描述
LOG_HEADER_FORMAT 4 redo日志的版本诚纸,在MySQL 5.7.21中該值永遠(yuǎn)為1
LOG_HEADER_PAD1 4 做字節(jié)填充用的撰筷,沒什么實(shí)際意義,忽略~
LOG_HEADER_START_LSN 8 標(biāo)記本redo日志文件開始的LSN值畦徘,也就是文件偏移量為2048字節(jié)初對應(yīng)的LSN值(關(guān)于什么是LSN我們稍后再看哈闭专,看不懂的先忽略)。
LOG_HEADER_CREATOR 32 一個(gè)字符串旧烧,標(biāo)記本redo日志文件的創(chuàng)建者是誰影钉。正常運(yùn)行時(shí)該值為MySQL的版本號,比如:"MySQL 5.7.21"掘剪,使用mysqlbackup命令創(chuàng)建的redo日志文件的該值為"ibbackup"和創(chuàng)建時(shí)間平委。
LOG_BLOCK_CHECKSUM 4 本block的校驗(yàn)值,所有block都有夺谁,我們不關(guān)心
  • checkpoint1:記錄關(guān)于checkpoint的一些屬性廉赔,看一下它的結(jié)構(gòu):
image.png

各個(gè)屬性的具體釋義如下:

  • checkpoint1:記錄關(guān)于checkpoint的一些屬性,看一下它的結(jié)構(gòu):

    ![image_1d4njq08pd2a5j9pc01qcn2ps7g.png-60.1kB][5]

    各個(gè)屬性的具體釋義如下:

    屬性名 長度(單位:字節(jié)) 描述
    LOG_CHECKPOINT_NO 8 服務(wù)器做checkpoint的編號匾鸥,每做一次checkpoint蜡塌,該值就加1。
    LOG_CHECKPOINT_LSN 8 服務(wù)器做checkpoint結(jié)束時(shí)對應(yīng)的LSN值勿负,系統(tǒng)奔潰恢復(fù)時(shí)將從該值開始馏艾。
    LOG_CHECKPOINT_OFFSET 8 上個(gè)屬性中的LSN值在redo日志文件組中的偏移量
    LOG_CHECKPOINT_LOG_BUF_SIZE 8 服務(wù)器在做checkpoint操作時(shí)對應(yīng)的log buffer的大小
    LOG_BLOCK_CHECKSUM 4 本block的校驗(yàn)值,所有block都有,我們不關(guān)心
  • 第三個(gè)block未使用琅摩,忽略~

  • checkpoint2:結(jié)構(gòu)和checkpoint1一樣铁孵。

Log Sequeue Number

自系統(tǒng)開始運(yùn)行,就不斷的在修改頁面房资,也就意味著會不斷的生成redo日志蜕劝。redo日志的量在不斷的遞增,就像人的年齡一樣轰异,自打出生起就不斷遞增岖沛,永遠(yuǎn)不可能縮減了。設(shè)計(jì)InnoDB的大叔為記錄已經(jīng)寫入的redo日志量搭独,設(shè)計(jì)了一個(gè)稱之為Log Sequeue Number的全局變量婴削,翻譯過來就是:日志序列號,簡稱lsn戳稽。不過不像人一出生的年齡是0歲,設(shè)計(jì)InnoDB的大叔規(guī)定初始的lsn值為8704(也就是一條redo日志也沒寫入時(shí)期升,lsn的值為8704)惊奇。

我們知道在向log buffer中寫入redo日志時(shí)不是一條一條寫入的,而是以一個(gè)mtr生成的一組redo日志為單位進(jìn)行寫入的播赁。而且實(shí)際上是把日志內(nèi)容寫在了log block body處颂郎。但是在統(tǒng)計(jì)lsn的增長量時(shí),是按照實(shí)際寫入的日志量加上占用的log block header和log block trailer來計(jì)算的容为。我們來看一個(gè)例子:

  • 系統(tǒng)第一次啟動(dòng)后初始化log buffer時(shí)乓序,buf_free(就是標(biāo)記下一條redo日志應(yīng)該寫入到log buffer的位置的變量)就會指向第一個(gè)block的偏移量為12字節(jié)(log block header的大小)的地方,那么lsn值也會跟著增加12:
image.png
  • 如果某個(gè)mtr產(chǎn)生的一組redo日志占用的存儲空間比較小,也就是待插入的block剩余空閑空間能容納這個(gè)mtr提交的日志時(shí)拯杠,lsn增長的量就是該mtr生成的redo日志占用的字節(jié)數(shù)斑举,就像這樣:
image.png

我們假設(shè)上圖中mtr_1產(chǎn)生的redo日志量為200字節(jié),那么lsn就要在8716的基礎(chǔ)上增加200寸士,變?yōu)?916。

  • 如果某個(gè)mtr產(chǎn)生的一組redo日志占用的存儲空間比較大,也就是待插入的block剩余空閑空間不足以容納這個(gè)mtr提交的日志時(shí)眨业,lsn增長的量就是該mtr生成的redo日志占用的字節(jié)數(shù)加上額外占用的log block header和log block trailer的字節(jié)數(shù),就像這樣:
image.png

我們假設(shè)上圖中mtr_2產(chǎn)生的redo日志量為1000字節(jié)沮协,為了將mtr_2產(chǎn)生的redo日志寫入log buffer龄捡,我們不得不額外多分配兩個(gè)block,所以lsn的值需要在8916的基礎(chǔ)上增加1000 + 12×2 + 4 × 2 = 1032慷暂。

從上邊的描述中可以看出來聘殖,每一組由mtr生成的redo日志都有一個(gè)唯一的LSN值與其對應(yīng),LSN值越小,說明redo日志產(chǎn)生的越早就斤。

flushed_to_disk_lsn

redo日志是首先寫到log buffer中悍募,之后才會被刷新到磁盤上的redo日志文件。所以設(shè)計(jì)InnoDB的大叔提出了一個(gè)稱之為buf_next_to_write的全局變量洋机,標(biāo)記當(dāng)前l(fā)og buffer中已經(jīng)有哪些日志被刷新到磁盤中了坠宴。畫個(gè)圖表示就是這樣:

image.png

我們前邊說lsn是表示當(dāng)前系統(tǒng)中寫入的redo日志量,這包括了寫到log buffer而沒有刷新到磁盤的日志绷旗,相應(yīng)的喜鼓,設(shè)計(jì)InnoDB的大叔提出了一個(gè)表示刷新到磁盤中的redo日志量的全局變量,稱之為flushed_to_disk_lsn衔肢。系統(tǒng)第一次啟動(dòng)時(shí)庄岖,該變量的值和初始的lsn值是相同的,都是8704角骤。隨著系統(tǒng)的運(yùn)行隅忿,redo日志被不斷寫入log buffer,但是并不會立即刷新到磁盤邦尊,lsn的值就和flushed_to_disk_lsn的值拉開了差距背桐。我們演示一下:

  • 系統(tǒng)第一次啟動(dòng)后,向log buffer中寫入了mtr_1蝉揍、mtr_2链峭、mtr_3這三個(gè)mtr產(chǎn)生的redo日志,假設(shè)這三個(gè)mtr開始和結(jié)束時(shí)對應(yīng)的lsn值分別是:
mtr_1:8716 ~ 8916
mtr_2:8916 ~ 9948
mtr_3:9948 ~ 10000

此時(shí)的lsn已經(jīng)增長到了10000又沾,但是由于沒有刷新操作弊仪,所以此時(shí)flushed_to_disk_lsn的值仍為8704,如圖:

image.png
  • 隨后進(jìn)行將log buffer中的block刷新到redo日志文件的操作杖刷,假設(shè)將mtr_1和mtr_2的日志刷新到磁盤励饵,那么flushed_to_disk_lsn就應(yīng)該增長mtr_1和mtr_2寫入的日志量,所以flushed_to_disk_lsn的值增長到了9948滑燃,如圖:
image.png

綜上所述曲横,當(dāng)有新的redo日志寫入到log buffer時(shí),首先lsn的值會增長不瓶,但flushed_to_disk_lsn不變禾嫉,隨后隨著不斷有l(wèi)og buffer中的日志被刷新到磁盤上,flushed_to_disk_lsn的值也跟著增長蚊丐。如果兩者的值相同時(shí)熙参,說明log buffer中的所有redo日志都已經(jīng)刷新到磁盤中了。

小貼士: 應(yīng)用程序向磁盤寫入文件時(shí)其實(shí)是先寫到操作系統(tǒng)的緩沖區(qū)中去麦备,如果某個(gè)寫入操作要等到操作系統(tǒng)確認(rèn)已經(jīng)寫到磁盤時(shí)才返回孽椰,那需要調(diào)用一下操作系統(tǒng)提供的fsync函數(shù)昭娩。其實(shí)只有當(dāng)系統(tǒng)執(zhí)行了fsync函數(shù)后,flushed_to_disk_lsn的值才會跟著增長黍匾,當(dāng)僅僅把log buffer中的日志寫入到操作系統(tǒng)緩沖區(qū)卻沒有顯式的刷新到磁盤時(shí)栏渺,另外的一個(gè)稱之為write_lsn的值跟著增長。不過為了大家理解上的方便锐涯,我們在講述時(shí)把flushed_to_disk_lsn和write_lsn的概念混淆了起來磕诊。

lsn值和redo日志文件偏移量的對應(yīng)關(guān)系

因?yàn)閘sn的值是代表系統(tǒng)寫入的redo日志量的一個(gè)總和,一個(gè)mtr中產(chǎn)生多少日志纹腌,lsn的值就增加多少(當(dāng)然有時(shí)候要加上log block header和log block trailer的大婿铡),這樣mtr產(chǎn)生的日志寫到磁盤中時(shí)升薯,很容易計(jì)算某一個(gè)lsn值在redo日志文件組中的偏移量莱褒,如圖:

image.png

初始時(shí)的LSN值是8704,對應(yīng)文件偏移量2048涎劈,之后每個(gè)mtr向磁盤中寫入多少字節(jié)日志广凸,lsn的值就增長多少。

flush鏈表中的LSN

我們知道一個(gè)mtr代表一次對底層頁面的原子訪問蛛枚,在訪問過程中可能會產(chǎn)生一組不可分割的redo日志谅海,在mtr結(jié)束時(shí),會把這一組redo日志寫入到log buffer中坤候。除此之外胁赢,在mtr結(jié)束時(shí)還有一件非常重要的事情要做企蹭,就是把在mtr執(zhí)行過程中可能修改過的頁面加入到Buffer Pool的flush鏈表白筹。為了防止大家早已忘記flush鏈表是個(gè)啥,我們再看一下圖:

image.png

當(dāng)?shù)谝淮涡薷哪硞€(gè)緩存在Buffer Pool中的頁面時(shí)谅摄,就會把這個(gè)頁面對應(yīng)的控制塊插入到flush鏈表的頭部徒河,之后再修改該頁面時(shí)由于它已經(jīng)在flush鏈表中了,就不再次插入了送漠。也就是說flush鏈表中的臟頁是按照頁面的第一次修改時(shí)間從大到小進(jìn)行排序的顽照。在這個(gè)過程中會在緩存頁對應(yīng)的控制塊中記錄兩個(gè)關(guān)于頁面何時(shí)修改的屬性:

  • oldest_modification:如果某個(gè)頁面被加載到Buffer Pool后進(jìn)行第一次修改,那么就將修改該頁面的mtr開始時(shí)對應(yīng)的lsn值寫入這個(gè)屬性闽寡。

  • newest_modification:每修改一次頁面代兵,都會將修改該頁面的mtr結(jié)束時(shí)對應(yīng)的lsn值寫入這個(gè)屬性。也就是說該屬性表示頁面最近一次修改后對應(yīng)的系統(tǒng)lsn值爷狈。

我們接著上邊嘮叨flushed_to_disk_lsn的例子看一下:

  • 假設(shè)mtr_1執(zhí)行過程中修改了頁a植影,那么在mtr_1執(zhí)行結(jié)束時(shí),就會將頁a對應(yīng)的控制塊加入到flush鏈表的頭部涎永。并且將mtr_1開始時(shí)對應(yīng)的lsn思币,也就是8716寫入頁a對應(yīng)的控制塊的oldest_modification屬性中鹿响,把mtr_1結(jié)束時(shí)對應(yīng)的lsn,也就是8916寫入頁a對應(yīng)的控制塊的newest_modification屬性中谷饿。畫個(gè)圖表示一下(為了讓圖片美觀一些惶我,我們把oldest_modification縮寫成了o_m,把newest_modification縮寫成了n_m):
image.png
  • 接著假設(shè)mtr_2執(zhí)行過程中又修改了頁b和頁c兩個(gè)頁面博投,那么在mtr_2執(zhí)行結(jié)束時(shí)绸贡,就會將頁b和頁c對應(yīng)的控制塊都加入到flush鏈表的頭部。并且將mtr_2開始時(shí)對應(yīng)的lsn贬堵,也就是8916寫入頁b和頁c對應(yīng)的控制塊的oldest_modification屬性中恃轩,把mtr_2結(jié)束時(shí)對應(yīng)的lsn,也就是9948寫入頁b和頁c對應(yīng)的控制塊的newest_modification屬性中黎做。畫個(gè)圖表示一下:
image.png

從圖中可以看出來叉跛,每次新插入到flush鏈表中的節(jié)點(diǎn)都是被放在了頭部,也就是說flush鏈表中前邊的臟頁修改的時(shí)間比較晚蒸殿,后邊的臟頁修改時(shí)間比較早筷厘。

  • 接著假設(shè)mtr_3執(zhí)行過程中修改了頁b和頁d,不過頁b之前已經(jīng)被修改過了宏所,所以它對應(yīng)的控制塊已經(jīng)被插入到了flush鏈表酥艳,所以在mtr_3執(zhí)行結(jié)束時(shí),只需要將頁d對應(yīng)的控制塊都加入到flush鏈表的頭部即可爬骤。所以需要將mtr_3開始時(shí)對應(yīng)的lsn充石,也就是9948寫入頁d對應(yīng)的控制塊的oldest_modification屬性中,把mtr_3結(jié)束時(shí)對應(yīng)的lsn霞玄,也就是10000寫入頁d對應(yīng)的控制塊的newest_modification屬性中骤铃。另外,由于頁b在mtr_3執(zhí)行過程中又發(fā)生了一次修改坷剧,所以需要更新頁b對應(yīng)的控制塊中newest_modification的值為10000惰爬。畫個(gè)圖表示一下:
image.png

總結(jié)一下上邊說的,就是:flush鏈表中的臟頁按照修改發(fā)生的時(shí)間順序進(jìn)行排序惫企,也就是按照oldest_modification代表的LSN值進(jìn)行排序撕瞧,被多次更新的頁面不會重復(fù)插入到flush鏈表中,但是會更新newest_modification屬性的值狞尔。

checkpoint

有一個(gè)很不幸的事實(shí)就是我們的redo日志文件組容量是有限的丛版,我們不得不選擇循環(huán)使用redo日志文件組中的文件,但是這會造成最后寫的redo日志與最開始寫的redo日志追尾偏序,這時(shí)應(yīng)該想到:redo日志只是為了系統(tǒng)奔潰后恢復(fù)臟頁用的页畦,如果對應(yīng)的臟頁已經(jīng)刷新到了磁盤,也就是說即使現(xiàn)在系統(tǒng)奔潰禽车,那么在重啟后也用不著使用redo日志恢復(fù)該頁面了寇漫,所以該redo日志也就沒有存在的必要了刊殉,那么它占用的磁盤空間就可以被后續(xù)的redo日志所重用。也就是說:判斷某些redo日志占用的磁盤空間是否可以覆蓋的依據(jù)就是它對應(yīng)的臟頁是否已經(jīng)刷新到磁盤里州胳。我們看一下前邊一直嘮叨的那個(gè)例子:

image.png

如圖记焊,雖然mtr_1和mtr_2生成的redo日志都已經(jīng)被寫到了磁盤上,但是它們修改的臟頁仍然留在Buffer Pool中栓撞,所以它們生成的redo日志在磁盤上的空間是不可以被覆蓋的遍膜。之后隨著系統(tǒng)的運(yùn)行,如果頁a被刷新到了磁盤瓤湘,那么它對應(yīng)的控制塊就會從flush鏈表中移除瓢颅,就像這樣子:

image.png

這樣mtr_1生成的redo日志就沒有用了,它們占用的磁盤空間就可以被覆蓋掉了弛说。設(shè)計(jì)InnoDB的大叔提出了一個(gè)全局變量checkpoint_lsn來代表當(dāng)前系統(tǒng)中可以被覆蓋的redo日志總量是多少挽懦,這個(gè)變量初始值也是8704。

比方說現(xiàn)在頁a被刷新到了磁盤木人,mtr_1生成的redo日志就可以被覆蓋了信柿,所以我們可以進(jìn)行一個(gè)增加checkpoint_lsn的操作,我們把這個(gè)過程稱之為做一次checkpoint醒第。做一次checkpoint其實(shí)可以分為兩個(gè)步驟:

  • 步驟一:計(jì)算一下當(dāng)前系統(tǒng)中可以被覆蓋的redo日志對應(yīng)的lsn值最大是多少渔嚷。

redo日志可以被覆蓋,意味著它對應(yīng)的臟頁被刷到了磁盤稠曼,只要我們計(jì)算出當(dāng)前系統(tǒng)中被最早修改的臟頁對應(yīng)的oldest_modification值形病,那凡是在系統(tǒng)lsn值小于該節(jié)點(diǎn)的oldest_modification值時(shí)產(chǎn)生的redo日志都是可以被覆蓋掉的,我們就把該臟頁的oldest_modification賦值給checkpoint_lsn霞幅。

比方說當(dāng)前系統(tǒng)中頁a已經(jīng)被刷新到磁盤漠吻,那么flush鏈表的尾節(jié)點(diǎn)就是頁c,該節(jié)點(diǎn)就是當(dāng)前系統(tǒng)中最早修改的臟頁了蝗岖,它的oldest_modification值為8916侥猩,我們就把8916賦值給checkpoint_lsn(也就是說在redo日志對應(yīng)的lsn值小于8916時(shí)就可以被覆蓋掉)榔至。

  • 步驟二:將checkpoint_lsn和對應(yīng)的redo日志文件組偏移量以及此次checkpint的編號寫到日志文件的管理信息(就是checkpoint1或者checkpoint2)中抵赢。

設(shè)計(jì)InnoDB的大叔維護(hù)了一個(gè)目前系統(tǒng)做了多少次checkpoint的變量checkpoint_no,每做一次checkpoint唧取,該變量的值就加1铅鲤。我們前邊說過計(jì)算一個(gè)lsn值對應(yīng)的redo日志文件組偏移量是很容易的,所以可以計(jì)算得到該checkpoint_lsn在redo日志文件組中對應(yīng)的偏移量checkpoint_offset枫弟,然后把這三個(gè)值都寫到redo日志文件組的管理信息中邢享。

我們說過,每一個(gè)redo日志文件都有2048個(gè)字節(jié)的管理信息淡诗,但是上述關(guān)于checkpoint的信息只會被寫到日志文件組的第一個(gè)日志文件的管理信息中骇塘。不過我們是存儲到checkpoint1中還是checkpoint2中呢伊履?設(shè)計(jì)InnoDB的大叔規(guī)定,當(dāng)checkpoint_no的值是偶數(shù)時(shí)款违,就寫到checkpoint1中唐瀑,是奇數(shù)時(shí),就寫到checkpoint2中插爹。

記錄完checkpoint的信息之后哄辣,redo日志文件組中各個(gè)lsn值的關(guān)系就像這樣:

image.png

批量從flush鏈表中刷出臟頁

我們在介紹Buffer Pool的時(shí)候說過,一般情況下都是后臺的線程在對LRU鏈表和flush鏈表進(jìn)行刷臟操作赠尾,這主要因?yàn)樗⑴K操作比較慢力穗,不想影響用戶線程處理請求。但是如果當(dāng)前系統(tǒng)修改頁面的操作十分頻繁气嫁,這樣就導(dǎo)致寫日志操作十分頻繁当窗,系統(tǒng)lsn值增長過快。如果后臺的刷臟操作不能將臟頁刷出寸宵,那么系統(tǒng)無法及時(shí)做checkpoint超全,可能就需要用戶線程同步的從flush鏈表中把那些最早修改的臟頁(oldest_modification最小的臟頁)刷新到磁盤,這樣這些臟頁對應(yīng)的redo日志就沒用了邓馒,然后就可以去做checkpoint了嘶朱。

查看系統(tǒng)中的各種LSN值

我們可以使用SHOW ENGINE INNODB STATUS命令查看當(dāng)前InnoDB存儲引擎中的各種LSN值的情況,比如:

mysql> SHOW ENGINE INNODB STATUS\G

(...省略前邊的許多狀態(tài))
LOG
---
Log sequence number 124476971
Log flushed up to   124099769
Pages flushed up to 124052503
Last checkpoint at  124052494
0 pending log flushes, 0 pending chkp writes
24 log i/o's done, 2.00 log i/o's/second
----------------------
(...省略后邊的許多狀態(tài))

其中:

  • Log sequence number:代表系統(tǒng)中的lsn值光酣,也就是當(dāng)前系統(tǒng)已經(jīng)寫入的redo日志量疏遏,包括寫入log buffer中的日志。

  • Log flushed up to:代表flushed_to_disk_lsn的值救军,也就是當(dāng)前系統(tǒng)已經(jīng)寫入磁盤的redo日志量财异。

  • Pages flushed up to:代表flush鏈表中被最早修改的那個(gè)頁面對應(yīng)的oldest_modification屬性值。

  • Last checkpoint at:當(dāng)前系統(tǒng)的checkpoint_lsn值唱遭。

innodb_flush_log_at_trx_commit的用法

我們前邊說為了保證事務(wù)的持久性戳寸,用戶線程在事務(wù)提交時(shí)需要將該事務(wù)執(zhí)行過程中產(chǎn)生的所有redo日志都刷新到磁盤上。這一條要求太狠了拷泽,會很明顯的降低數(shù)據(jù)庫性能疫鹊。如果有的同學(xué)對事務(wù)的持久性要求不是那么強(qiáng)烈的話,可以選擇修改一個(gè)稱為innodb_flush_log_at_trx_commit的系統(tǒng)變量的值司致,該變量有3個(gè)可選的值:

  • 0:當(dāng)該系統(tǒng)變量值為0時(shí)拆吆,表示在事務(wù)提交時(shí)不立即向磁盤中同步redo日志,這個(gè)任務(wù)是交給后臺線程做的脂矫。

    這樣很明顯會加快請求處理速度枣耀,但是如果事務(wù)提交后服務(wù)器掛了,后臺線程沒有及時(shí)將redo日志刷新到磁盤庭再,那么該事務(wù)對頁面的修改會丟失捞奕。

  • 1:當(dāng)該系統(tǒng)變量值為1時(shí)牺堰,表示在事務(wù)提交時(shí)需要將redo日志同步到磁盤,可以保證事務(wù)的持久性颅围。1也是innodb_flush_log_at_trx_commit的默認(rèn)值萌焰。

  • 2:當(dāng)該系統(tǒng)變量值為2時(shí),表示在事務(wù)提交時(shí)需要將redo日志寫到操作系統(tǒng)的緩沖區(qū)中谷浅,但并不需要保證將日志真正的刷新到磁盤扒俯。

    這種情況下如果數(shù)據(jù)庫掛了,操作系統(tǒng)沒掛的話一疯,事務(wù)的持久性還是可以保證的撼玄,但是操作系統(tǒng)也掛了的話,那就不能保證持久性了墩邀。

崩潰恢復(fù)

在服務(wù)器不掛的情況下掌猛,redo日志簡直就是個(gè)大累贅,不僅沒用眉睹,反而讓性能變得更差荔茬。但是萬一,我說萬一啊竹海,萬一數(shù)據(jù)庫掛了慕蔚,那redo日志可是個(gè)寶了,我們就可以在重啟時(shí)根據(jù)redo日志中的記錄就可以將頁面恢復(fù)到系統(tǒng)奔潰前的狀態(tài)斋配。我們接下來大致看一下恢復(fù)過程是個(gè)啥樣孔飒。

確定恢復(fù)的起點(diǎn)

我們前邊說過,checkpoint_lsn之前的redo日志都可以被覆蓋艰争,也就是說這些redo日志對應(yīng)的臟頁都已經(jīng)被刷新到磁盤中了坏瞄,既然它們已經(jīng)被刷盤,我們就沒必要恢復(fù)它們了甩卓。對于checkpoint_lsn之后的redo日志鸠匀,它們對應(yīng)的臟頁可能沒被刷盤,也可能被刷盤了逾柿,我們不能確定缀棍,所以需要從checkpoint_lsn開始讀取redo日志來恢復(fù)頁面。

當(dāng)然鹿寻,redo日志文件組的第一個(gè)文件的管理信息中有兩個(gè)block都存儲了checkpoint_lsn的信息睦柴,我們當(dāng)然是要選取最近發(fā)生的那次checkpoint的信息诽凌。衡量checkpoint發(fā)生時(shí)間早晚的信息就是所謂的checkpoint_no毡熏,我們只要把checkpoint1和checkpoint2這兩個(gè)block中的checkpoint_no值讀出來比一下大小,哪個(gè)的checkpoint_no值更大侣诵,說明哪個(gè)block存儲的就是最近的一次checkpoint信息痢法。這樣我們就能拿到最近發(fā)生的checkpoint對應(yīng)的checkpoint_lsn值以及它在redo日志文件組中的偏移量checkpoint_offset狱窘。

確定恢復(fù)的終點(diǎn)

redo日志恢復(fù)的起點(diǎn)確定了,那終點(diǎn)是哪個(gè)呢财搁?這個(gè)還得從block的結(jié)構(gòu)說起蘸炸。我們說在寫redo日志的時(shí)候都是順序?qū)懙模瑢憹M了一個(gè)block之后會再往下一個(gè)block中寫:

image.png

普通block的log block header部分有一個(gè)稱之為LOG_BLOCK_HDR_DATA_LEN的屬性尖奔,該屬性值記錄了當(dāng)前block里使用了多少字節(jié)的空間搭儒。對于被填滿的block來說,該值永遠(yuǎn)為512提茁。如果該屬性的值不為512淹禾,那么就是它了,它就是此次奔潰恢復(fù)中需要掃描的最后一個(gè)block茴扁。

怎么恢復(fù)

確定了需要掃描哪些redo日志進(jìn)行奔潰恢復(fù)之后铃岔,接下來就是怎么進(jìn)行恢復(fù)了。假設(shè)現(xiàn)在的redo日志文件中有5條redo日志峭火,如圖:

image.png

由于redo 0在checkpoint_lsn后邊毁习,恢復(fù)時(shí)可以不管它。我們現(xiàn)在可以按照redo日志的順序依次掃描checkpoint_lsn之后的各條redo日志卖丸,按照日志中記載的內(nèi)容將對應(yīng)的頁面恢復(fù)出來纺且。這樣沒什么問題,不過設(shè)計(jì)InnoDB的大叔還是想了一些辦法加快這個(gè)恢復(fù)的過程:

  • 使用哈希表

    根據(jù)redo日志的space IDpage number屬性計(jì)算出散列值稍浆,把space IDpage number相同的redo日志放到哈希表的同一個(gè)槽里隆檀,如果有多個(gè)space IDpage number都相同的redo日志,那么它們之間使用鏈表連接起來粹湃,按照生成的先后順序鏈接起來的恐仑,如圖所示:

image.png

之后就可以遍歷哈希表,因?yàn)閷ν粋€(gè)頁面進(jìn)行修改的redo日志都放在了一個(gè)槽里为鳄,所以可以一次性將一個(gè)頁面修復(fù)好(避免了很多讀取頁面的隨機(jī)IO)裳仆,這樣可以加快恢復(fù)速度。另外需要注意一點(diǎn)的是孤钦,同一個(gè)頁面的redo日志是按照生成時(shí)間順序進(jìn)行排序的歧斟,所以恢復(fù)的時(shí)候也是按照這個(gè)順序進(jìn)行恢復(fù),如果不按照生成時(shí)間順序進(jìn)行排序的話偏形,那么可能出現(xiàn)錯(cuò)誤静袖。比如原先的修改操作是先插入一條記錄,再刪除該條記錄俊扭,如果恢復(fù)時(shí)不按照這個(gè)順序來队橙,就可能變成先刪除一條記錄,再插入一條記錄,這顯然是錯(cuò)誤的捐康。

  • 跳過已經(jīng)刷新到磁盤的頁面

我們前邊說過仇矾,checkpoint_lsn之前的redo日志對應(yīng)的臟頁確定都已經(jīng)刷到磁盤了,但是checkpoint_lsn之后的redo日志我們不能確定是否已經(jīng)刷到磁盤解总,主要是因?yàn)樵谧罱龅囊淮蝐heckpoint后贮匕,可能后臺線程又不斷的從LRU鏈表和flush鏈表中將一些臟頁刷出Buffer Pool。這些在checkpoint_lsn之后的redo日志花枫,如果它們對應(yīng)的臟頁在奔潰發(fā)生時(shí)已經(jīng)刷新到磁盤刻盐,那在恢復(fù)時(shí)也就沒有必要根據(jù)redo日志的內(nèi)容修改該頁面了。

那在恢復(fù)時(shí)怎么知道某個(gè)redo日志對應(yīng)的臟頁是否在奔潰發(fā)生時(shí)已經(jīng)刷新到磁盤了呢劳翰?這還得從頁面的結(jié)構(gòu)說起隙疚,我們前邊說過每個(gè)頁面都有一個(gè)稱之為File Header的部分,在File Header里有一個(gè)稱之為FIL_PAGE_LSN的屬性磕道,該屬性記載了最近一次修改頁面時(shí)對應(yīng)的lsn值(其實(shí)就是頁面控制塊中的newest_modification值)供屉。如果在做了某次checkpoint之后有臟頁被刷新到磁盤中,那么該頁對應(yīng)的FIL_PAGE_LSN代表的lsn值肯定大于checkpoint_lsn的值溺蕉,凡是符合這種情況的頁面就不需要重復(fù)執(zhí)行l(wèi)sn值小于FIL_PAGE_LSN的redo日志了伶丐,所以更進(jìn)一步提升了奔潰恢復(fù)的速度。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疯特,一起剝皮案震驚了整個(gè)濱河市哗魂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌漓雅,老刑警劉巖录别,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異邻吞,居然都是意外死亡组题,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門抱冷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來崔列,“玉大人,你說我怎么就攤上這事旺遮≌匝叮” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵耿眉,是天一觀的道長边翼。 經(jīng)常有香客問我,道長鸣剪,這世上最難降的妖魔是什么组底? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任丈积,我火速辦了婚禮,結(jié)果婚禮上斤寇,老公的妹妹穿的比我還像新娘桶癣。我一直安慰自己拥褂,他們只是感情好娘锁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著饺鹃,像睡著了一般莫秆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上悔详,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天镊屎,我揣著相機(jī)與錄音,去河邊找鬼茄螃。 笑死缝驳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的归苍。 我是一名探鬼主播用狱,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拼弃!你這毒婦竟也來了夏伊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤吻氧,失蹤者是張志新(化名)和其女友劉穎溺忧,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盯孙,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鲁森,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了振惰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刀森。...
    茶點(diǎn)故事閱讀 39,981評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖报账,靈堂內(nèi)的尸體忽然破棺而出研底,到底是詐尸還是另有隱情,我是刑警寧澤透罢,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布榜晦,位于F島的核電站,受9級特大地震影響羽圃,放射性物質(zhì)發(fā)生泄漏乾胶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望识窿。 院中可真熱鬧斩郎,春花似錦、人聲如沸喻频。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽甥温。三九已至锻煌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間姻蚓,已是汗流浹背宋梧。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狰挡,地道東北人捂龄。 一個(gè)月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像加叁,于是被迫代替她去往敵國和親倦沧。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評論 2 355