故事是這樣開始的层皱,很久很久以前伴箩,在一個(gè)月結(jié)(喝酒的人一個(gè)月結(jié)一次賬)酒店的老板是這樣記賬的:每次一個(gè)客戶進(jìn)來買酒识椰,老板都會(huì)找出賬本绝葡,然后呢再找到這個(gè)人,在這個(gè)人的名字下面記一下買酒的金額腹鹉、日期藏畅,記完之后把酒給到客戶。后來酒店的生意越來越好种蘸,老板發(fā)現(xiàn)每次客戶來都找到賬本再找到這個(gè)人名字墓赴,記錄賒賬信息,效率十分低下航瞭,忙不過來诫硕。于是老板想到一個(gè)新的辦法:他找來一個(gè)黑板,每次客戶來呢刊侯,先把這個(gè)人的賒賬信息記錄在黑板上章办,等到自己空閑的時(shí)候再把賒賬信息一條一條更新到賬本上。老板這么一搞,一天接待的客戶也多了藕届,這真是一個(gè)好辦法挪蹭。``
mysql更新語句會(huì)涉及到寫磁盤的過程,如果每次更新語句都去寫磁盤就像酒店老板每次找到賬本寫賒賬信息一樣休偶,那必然很影響mysql處理速度梁厉,更不可能在現(xiàn)在高并發(fā)的場景下滿足要求。為了提高處理速度踏兜,mysql實(shí)際上也是采用了先寫黑板再寫賬本的方法词顾,黑板和賬本的配合過程,就是mysql 中常說的WAL(Write-Ahead Logging)技術(shù)碱妆。其實(shí)就是先寫日志再寫磁盤肉盹。這里的日志在mysql中叫redo log,對(duì)應(yīng)的就是酒店老板的小黑板疹尾,磁盤對(duì)應(yīng)的就是酒店老板的賬本上忍。其實(shí)呢mysql 的更新過程不僅有redo log還涉及到binlog,那下面我們先介紹一下 redo log 纳本。
redo log
redo log記錄的是物理日志窍蓝,是對(duì)數(shù)據(jù)頁某個(gè)位置的修改,所以說redo log 會(huì)記修改的數(shù)據(jù)頁的編號(hào)(page no)饮醇。這里插一句 redo log 也會(huì)記錄LSN(log sequence number)
它抱,LSN 是單調(diào)遞增的,每次寫入長度為length的redo log朴艰,LSN就會(huì)加length观蓄,來標(biāo)識(shí)每次redo log 的寫入位點(diǎn),數(shù)據(jù)頁也會(huì)記錄當(dāng)前頁最后一次修改的LSN祠墅,它記錄在數(shù)據(jù)頁的頭部侮穿,它的主要目的是用于在恢復(fù)數(shù)據(jù)時(shí)對(duì)比redolog日志的LSN號(hào)決定是否對(duì)該頁進(jìn)行恢復(fù)數(shù)據(jù),LSN把一個(gè)事務(wù)開始到恢復(fù)的過程串聯(lián)起來了毁嗦。前面說的LSN亲茅,checkpoint也是有記錄的,checkpoint位于redo log file 文件file_header 里面 狗准。innodb 引擎在寫redo log 的時(shí)候先把redo log 寫到 redo log buffer 中(redo log buffer 的大小由)克锣,寫的時(shí)候是一個(gè)一個(gè)的redo log block ,redo log block每個(gè)大小是512字節(jié)腔长,其結(jié)構(gòu)如下:
其中l(wèi)og block中492字節(jié)的部分是log body袭祟,該log body的格式分為4部分:
- redo_log_type:redo log的日志類型,占用1個(gè)字節(jié)捞附。
- space:空間的ID巾乳,采用壓縮的方式后您没,占用的空間可能小于4字節(jié)。
- page_no:頁的偏移量
- redo_log_body 重做日志的數(shù)據(jù)部分胆绊,恢復(fù)時(shí)會(huì)調(diào)用相應(yīng)的函數(shù)進(jìn)行解析氨鹏。例如insert語句和delete語句寫入redo log的內(nèi)容是不一樣的。
在刷盤的時(shí)候压状,會(huì)將 redo log buffer (大小由innodb_log_buffer_size
控制)中的日志塊寫入redo log file 中 就是我們經(jīng)常在/data 目錄中看到的以ib_logfile
開頭的文件仆抵,ib_logfile 文件的大小由innodb_log_file_size
控制,個(gè)數(shù)由innodb_log_files_in_group
控制何缓,ib_logfile 文件之間的關(guān)系是他們是同屬于一個(gè)組肢础,文件之間通過鏈表鏈接 在組內(nèi)形成一個(gè)環(huán),就這樣覆蓋寫碌廓,實(shí)際上是這樣存在的:
邏輯上是這樣存在的:
這里也要說一下圖中 write pos
和check point
的含義:
- write pos 它表示的是日志的當(dāng)前寫入位置,一邊寫一邊后移剩盒,當(dāng)寫到ib_logfile_3末尾的時(shí)候谷婆,再繼續(xù)寫到ib_logfile_0
- checkpoint 是當(dāng)前要擦除的位置,也是往后移動(dòng)的辽聊,擦除前要把日志更新到數(shù)據(jù)文件中(就是磁盤中的數(shù)據(jù)頁)
如果checkpoint 追上write pos 纪挎,那么表示已經(jīng)沒有地方來寫日志了,這個(gè)時(shí)候不能再執(zhí)行更新跟匆,需要將checkpoint 往后移動(dòng)异袄,移動(dòng)的部分就是刷臟頁(這個(gè)過程在下一節(jié)講),有了 redo log玛臂,mysql就有了crash_safe 的能力烤蜕,就是說innodb 能夠保證數(shù)據(jù)庫發(fā)生異常重啟,數(shù)據(jù)不會(huì)丟失迹冤。
這里還有一個(gè)問題 relog buffer 里面的日志塊什么時(shí)候?qū)懭?redo log 文件呢(既ib_logfile文件)讽营?
這里要注意的是沒有提交事務(wù)的redo log 也是可能寫入到磁盤的,所以說我們分兩大類來討論寫盤問題
-
事務(wù)已提交
為了控制 redo log 的寫入策略泡徙,InnoDB 通過 innodb_flush_log_at_trx_commit 參數(shù)橱鹏,來控制redo log的寫入策略,它有三種可能取值:
- 設(shè)置為 0 的時(shí)候堪藐,表示每次事務(wù)提交時(shí)都只是把 redo log 留在 redo log buffer 中 ;
- 設(shè)置為 1 的時(shí)候莉兰,表示每次事務(wù)提交時(shí)都將 redo log 直接持久化到磁盤;
- 設(shè)置為 2 的時(shí)候礁竞,表示每次事務(wù)提交時(shí)都只是把 redo log 寫到 page cache糖荒。
InnoDB,后臺(tái)有一個(gè)線程每1s 就會(huì)把redo buffer 里面的日志調(diào)用write寫到文件系統(tǒng)的page cache 里面苏章,然后調(diào)用fsyn持久化到磁盤寂嘉,因?yàn)槭聞?wù)在執(zhí)行過程中寫的日志都在 redo buffer 里面奏瞬,所以所一個(gè)未完成(未提交)的事務(wù)的日志是可能持久化到磁盤的
-
事務(wù)未提交
除了上面說的定時(shí)線程會(huì)將未提交的事務(wù)的日志持久化到磁盤外,還有兩種情況也會(huì)將未提交事務(wù)的日志持久化到磁盤
- 當(dāng)redo log buffer 的已用空間超過 innodb_log_buffer_size 規(guī)定空間一半的時(shí)候泉孩,后臺(tái)線程會(huì)主動(dòng)寫盤硼端,但是這里注意的是這個(gè)寫盤只是調(diào)用了write 沒有調(diào)用 fsync,所以說只是寫到了 page_cache 里面寓搬。
- 并行的事務(wù)提交的時(shí)候珍昨,順帶將這個(gè)事務(wù)的 redo log buffer 持久化到磁盤。假設(shè)一個(gè)事務(wù) A 執(zhí)行到一半句喷,已經(jīng)寫了一些 redo log 到 buffer 中镣典,這時(shí)候有另外一個(gè)線程的事務(wù) B 提交,如果 innodb_flush_log_at_trx_commit 設(shè)置的是 1唾琼,那么按照這個(gè)參數(shù)的邏輯兄春,事務(wù) B 要把 redo log buffer 里的日志全部持久化到磁盤。這時(shí)候锡溯,就會(huì)帶上事務(wù) A 在 redo log buffer 里的日志一起持久化到磁盤赶舆。
binlog
binlog 也是日志,它是server 層記錄的日志祭饭。我們來比較一下它和redo log 日志文件的不同
- redo log 是 引擎層產(chǎn)生的芜茵,binlog 是由server 層產(chǎn)生的,所有引擎共用
- redo log 它是循環(huán)寫倡蝙,binlog 是追加寫九串,它不會(huì)覆蓋數(shù)據(jù),寫完之后再換到寫一個(gè)問價(jià)寫
- redo log 是物理日志寺鸥,記錄的是“在某個(gè)數(shù)據(jù)頁上做了什么修改”猪钮;binlog 是邏輯日志,記錄的是這個(gè)語句的原始邏輯析既,比如“給 ID=2 這一行的 c 字段加 1 ”躬贡。
binlog 記錄有三種格式:statement、row眼坏、mixed
- statement 記錄的是執(zhí)行語句 主從復(fù)制時(shí)可能會(huì)出現(xiàn)問題
- row 記錄要修改的數(shù)據(jù) 缺點(diǎn)就是 日志文件比較大拂玻, 優(yōu)點(diǎn)就是 數(shù)據(jù)恢復(fù)
- mixed mysql 會(huì)根據(jù)執(zhí)行的每一條具體的 SQL 語句來區(qū)分對(duì)待記錄的日志形式,也就是在 statement 和 row 之間選擇一種
binlog 刷盤過程:
binlog 的寫入邏輯是這樣的:事務(wù)執(zhí)行過程中宰译,先把日志寫到 binlog cache檐蚜,事務(wù)提交的時(shí)候,再把 binlog cache 寫到 binlog 文件中沿侈。
一個(gè)事務(wù)的 binlog 是不能被拆開的闯第,因此不論這個(gè)事務(wù)多大,也要確保一次性寫入缀拭。系統(tǒng)給 binlog cache 分配了一片內(nèi)存咳短,每個(gè)線程一個(gè)填帽,參數(shù) binlog_cache_size 用于控制單個(gè)線程內(nèi) binlog cache 所占內(nèi)存的大小。如果超過了這個(gè)參數(shù)規(guī)定的大小咙好,就要暫存到磁盤篡腌。事務(wù)提交的時(shí)候,執(zhí)行器把 binlog cache 里的完整事務(wù)寫入到 binlog 中勾效,并清空 binlog cache嘹悼。
每個(gè)線程有自己 binlog cache,但是共用同一份 binlog 文件层宫。
binlog刷盤的時(shí)機(jī)是由參數(shù) sync_binlog 控制的:
sync_binlog=0 的時(shí)候杨伙,表示每次提交事務(wù)都只 write,不 fsync萌腿;
sync_binlog=1 的時(shí)候限匣,表示每次提交事務(wù)都會(huì)執(zhí)行 fsync;
sync_binlog=N(N>1) 的時(shí)候毁菱,表示每次提交事務(wù)都 write膛腐,但累積 N 個(gè)事務(wù)后才 fsync。因此鼎俘,在出現(xiàn) IO 瓶頸的場景里,將 sync_binlog 設(shè)置成一個(gè)比較大的值辩涝,可以提升性能贸伐。
在實(shí)際的業(yè)務(wù)場景中,考慮到丟失日志量的可控性怔揩,一般不建議將這個(gè)參數(shù)設(shè)成 0捉邢,比較常見的是將其設(shè)置為 100~1000 中的某個(gè)數(shù)值。但是商膊,將 sync_binlog 設(shè)置為 N伏伐,對(duì)應(yīng)的風(fēng)險(xiǎn)是:如果主機(jī)發(fā)生異常重啟,會(huì)丟失最近 N 個(gè)事務(wù)的 binlog 日志晕拆。
mysql 更新流程
講完了 redo log 和 binlog ,現(xiàn)在我們有了基礎(chǔ)知識(shí)藐翎,那我們現(xiàn)在就來看看更新mysql的更新過程是怎樣的?
未避免流程差異化太大实幕,這里我們?cè)O(shè)置一個(gè)前提吝镣,mysql 的版本是5.7,非自動(dòng)提交(與自動(dòng)提交差別不大 主要是為了更清楚的描述整個(gè)過程)昆庇,sync_binlog =1末贾,innodb_flush_log_at_trx_commit = 1,binlog 是打開的整吆,現(xiàn)在我們拿語句UPDATE t set c= 20 where id =2;
來說明過程
執(zhí)行器首先調(diào)用引擎層的接口獲得 id = 2 的數(shù)據(jù)拱撵,引擎層的內(nèi)存如果存在id = 2 這一行的頁辉川,那么直接返回給執(zhí)行器 如果不存在那么 從磁盤中加載該頁 并放在內(nèi)存中 然后再返回給server 層
執(zhí)行器獲得數(shù)據(jù)后,對(duì)字段C 設(shè)置為20 拴测,并再次調(diào)用引擎層接口乓旗,引擎層先將本次要修改數(shù)據(jù)的原始數(shù)據(jù)寫到undo log 中,以防回滾昼扛,然后將本次修改記錄在 redo log 中 并存于buffer 中
事務(wù)進(jìn)入提交階段(這里用到了兩階段提交)寸齐,首先將redo buffer 里面的日志寫到磁盤,并標(biāo)記為prepare狀態(tài)抄谐,并告訴執(zhí)行器 我已經(jīng)提交了渺鹦,你也可以提交了,這個(gè)時(shí)候提交的第一階段完成
開始第二階段的提交蛹含,執(zhí)行器生成這個(gè)操作的binglog毅厚,并將binglog 也寫到磁盤 這個(gè)時(shí)候xid 也寫入到了binlog,binglog 落盤后浦箱,執(zhí)行器再調(diào)用引擎層的提交事務(wù)接口吸耿,將redo log 標(biāo)記為commit 狀態(tài),注意的是這個(gè)時(shí)候redo log 數(shù)據(jù)不用落盤
更新完成
為什么是兩階段提交
-
先寫 redo log 后寫 binlog
假設(shè)在 redo log 寫完酷窥,binlog 還沒有寫完的時(shí)候咽安,MySQL 進(jìn)程異常重啟。由于我們前面說過的蓬推,redo log 寫完之后妆棒,系統(tǒng)即使崩潰,仍然能夠把數(shù)據(jù)恢復(fù)回來沸伏,所以恢復(fù)后這一行 c 的值是 1糕珊。但是由于 binlog 沒寫完就 crash 了,這時(shí)候 binlog 里面就沒有記錄這個(gè)語句毅糟。因此红选,之后備份日志的時(shí)候,存起來的 binlog 里面就沒有這條語句姆另。然后你會(huì)發(fā)現(xiàn)喇肋,如果需要用這個(gè) binlog 來恢復(fù)臨時(shí)庫的話,由于這個(gè)語句的 binlog 丟失蜕青,這個(gè)臨時(shí)庫就會(huì)少了這一次更新苟蹈,恢復(fù)出來的這一行 c 的值就是 0,與原庫的值不同右核。
-
先寫 binlog 后寫 redo log慧脱。
如果在 binlog 寫完之后 crash,由于 redo log 還沒寫贺喝,崩潰恢復(fù)以后這個(gè)事務(wù)無效菱鸥,所以這一行 c 的值是 0宗兼。但是 binlog 里面已經(jīng)記錄了“把 c 從 0 改成 1”這個(gè)日志。所以氮采,在之后用 binlog 來恢復(fù)的時(shí)候就多了一個(gè)事務(wù)出來殷绍,恢復(fù)出來的這一行 c 的值就是 1,與原庫的值不同鹊漠。
系統(tǒng)重啟主到,數(shù)據(jù)恢復(fù)過程
數(shù)據(jù)庫恢復(fù)后會(huì)判斷redo log的事務(wù)是不是完整的,如果不是則根據(jù)undo log回滾躯概;如果是完整的并且是prepare狀態(tài)登钥,則進(jìn)一步判斷對(duì)應(yīng)的事務(wù)binlog是不是完整的,如果不完整則一樣根據(jù)undo log進(jìn)行回娶靡,如果是binlog是完整的就進(jìn)行提交