
???????假設MySQL需要執(zhí)行一條更新語句:update T set c = c + 1 where id = 2,在執(zhí)行這條更新語句之前,要先連接數(shù)據(jù)庫,這是連接器的工作。而在一張表上有更新的時候,跟這個表有關的查詢緩存會失效,所以這條語句就會把表T上所有緩存結(jié)果都清空,這也是建議不使用查詢緩存的原因之一。接下來,分析器會通過詞法分析和語法分析知道這是一條更新語句。優(yōu)化器決定要使用ID這個索引。然后執(zhí)行器負責具體執(zhí)行,找到這一行,然后更新。
???????更新語句的流程會涉及到兩個重要的日志模塊,即redo log(重做日志)和binlog(歸檔日志)。
重做日志redo log
???????MySQL在更新的時候,使用的是WAL(Write-Ahead Logging)技術(shù),即先寫日志,再寫磁盤。具體來說,當有一條記錄需要更新的時候,InnoDB引擎就會先把記錄寫到redo log里面,并更新內(nèi)存,這個時候更新就算完成了。同時,InnoDB引擎會在適當?shù)臅r候,將這個操作記錄更新到磁盤里面,而這個更新往往是在系統(tǒng)比較空閑的時候做。
???????InnoDB的redo log是固定大小的,比如可以配置為一組4個文件,每個文件的大小是1GB,那么這塊redo log總共就可以記錄4GB的操作。從頭開始寫,寫到末尾就又回到開頭循環(huán)寫,如下所示:

???????write pos是當前記錄的位置,一邊寫一邊往后移,寫到第三號文件末尾后就回到第零號文件開頭。checkpoint是當前要擦除的位置,也是往后推移并且循環(huán)的,擦除記錄前要把記錄更新到數(shù)據(jù)文件。write pos和checkpoint之間的空著的部分,可以用來記錄新的操作。如果write pos追上checkpoint,表示redo log文件滿了,這時候不能在執(zhí)行新的更新,得停下來先刪除一些記錄,把checkpoint推進一下。
???????有了redo log,InnoDB就可以保證即使數(shù)據(jù)庫發(fā)生異常重啟,之前提交的記錄都不會丟失,該能力成為crash-safe。
binlog歸檔日志
???????從MySQL的整體來看,分為如下的兩部分:Server層和存儲引擎層。前者主要是做MySQL功能層面的事情,后者負責存儲相關的具體事宜。上述的redo log是InnoDB引擎特有的日志,而Server層也有自己的日志,稱為binlog,即歸檔日志。
???????MySQL有兩份日志的原因,是因為最開始MySQL里并沒有InnoDB引擎。MySQL自帶的引擎是MyISAM,但是MyISAM沒有crash-safe的能力,binlog日志只能由于歸檔(歸檔,指的是將處理完并且具有保存價值的事情或文件經(jīng)系統(tǒng)整理后交檔案室保存?zhèn)浒傅倪^程)。而InnoDB是另一個公司以插件形式引入MySQL的,既然只依靠binlog是沒有crash-safe能力的,所以InnoDB使用另外一套日志系統(tǒng),即redo log來實現(xiàn)crash-safe能力。
???????redo log和binlog兩者的區(qū)別:
- redo log是InnoDB引擎特有的;binlog是MySQL的Server層實現(xiàn)的,所有引擎都可以使用;
- redo log是物理日志,記錄的是“在某個數(shù)據(jù)頁上做了什么修改”;binlog是邏輯日志,記錄的是SQL語句的原始邏輯。
-
redo log是循環(huán)寫的,空間固定會用完;binlog是可以追加寫入的?!白芳訉憽笔侵竍inlog文件寫到一定大小后會切換到下一個,并不會覆蓋以前的日志。
???????接下來看看,執(zhí)行器和InnoDB引擎執(zhí)行開篇的update語句時的內(nèi)部流程。如下圖:
update語句執(zhí)行流程 - 執(zhí)行器先找引擎去ID=2這一行。ID是主鍵,引擎直接用樹搜索找到這行。如果ID=2這一行所在的數(shù)據(jù)頁本來就在內(nèi)存中,就直接返回給執(zhí)行器;否則,需要先從磁盤讀入內(nèi)存,然后再返回。
- 執(zhí)行器拿到拿到引擎給的行數(shù)據(jù),把這個值加上1,得到新的一行數(shù)據(jù),再調(diào)用引擎接口寫入數(shù)據(jù)這樣新數(shù)據(jù)。
- 引擎將這行新數(shù)據(jù)更新到內(nèi)存中,同時將這個更新操作記錄到redo log里面,此時redo log處于prepare狀態(tài)。然后告知執(zhí)行器執(zhí)行完成了,隨時可以提交事務。
- 執(zhí)行器生成這個操作的binlog,并把binlog寫入磁盤。
- 執(zhí)行器調(diào)用引擎的提交事務接口,引擎把剛剛寫入的redo log改成提交(commit)狀態(tài),更新完成。
兩階段提交
???????從上圖中,看到redo log的寫入拆成了兩個步驟:prepare和commit,即“兩階段提交”,這是為了讓兩份日志之間的邏輯一致。
???????如果不使用兩階段提交,即先寫完redo log再寫binlog,或者先寫binlog再寫redo log,會有什么問題呢?以上文中的update語句為例。假設當前ID=2的行,字段c的值是0,再假設執(zhí)行update語句過程中在寫完第一個日志后,第二個日志還沒有寫完期間發(fā)生了crash,會出現(xiàn)什么情況呢?
- 先寫redo log,后寫binlog。
???????假設在redo log寫完,binlog還沒有寫完時候,MySQL進程異常重啟。由于我們前面說過的,redo log寫完之后,系統(tǒng)即使崩潰,仍然能夠把數(shù)據(jù)恢復回來,所以這一行c的值是1。但是由于binlog沒寫完就crash了,這時候binlog里面就沒有記錄這個語句。因此,之后備份日志的時候,存起來的binlog里面就沒有這條語句。然后你會發(fā)現(xiàn),如果需要用這個binlog來恢復臨時庫的話,由于這個語句的binlog丟失,這個臨時庫就會少了這一次更新,恢復出來的這一行c的值就是0,與原庫的值不同。 - 先寫binlog后寫redo log
???????如果在binlog寫完之后crash,由于redo log還沒寫,崩潰恢復以后這個事務無效,所以這一行c的值是0。但是binlog里面已經(jīng)記錄了“把c從0改成1”這個日志。所以,在之后用binlog來恢復的時候就多了一個事務出來,恢復出來的這一行c的值就是1,與原庫的值不同。
???????可以看到,如果不使用“兩階段提交”,那么數(shù)據(jù)庫的狀態(tài)就有可能和用它的日志恢復出來的庫的狀態(tài)不一致。
總結(jié)
???????今天介紹了MySQL里面最重要的兩個日志,即物理日志redo log和邏輯日志binlog。
???????redo log用于保證crash-safe能力。innodb_flush_at_trx_commit這個參數(shù)設置成1的時候,表示每次事務的redo log都直接持久化到磁盤。這個參數(shù)建議設置成1,這樣可以保證MySQL異常重啟之后數(shù)據(jù)不丟失。sync_binlog這個參數(shù)設置成1的時候,表示每次事務的binlog都持久化到磁盤。這個參數(shù)也建議設置成1,這樣可以保證MySQL異常重啟之后binlog不丟失。
