MySQL筆記(6)-- SQL更新語句日志系統流程


一、背景

  在上一篇【MySQL筆記(5)-- SQL執行流程,MySQL體系結構】中講述了select查詢語句在MySQL體系中的運行流程,從連接器開始,到分析器、優化器、執行器等,最后到達存儲引擎。那么對於update更新語句來說對應的流程又是怎樣的呢,今天我們來探討下更新跟查詢之間的區別。

二、更新語句的執行流程

當我們創建一張表時:

create table T(ID int primary key,c int);

如果這張表在創建完后插入了一些數據,現在要對一條數據執行更新操作:

update T set c=c+1 where ID=2;

 

它在MySQL服務端中執行的流程還是跟查詢一樣,如圖所示:

  我們前面說了,在執行更新操作時,會去緩沖器把跟這個表T有關的緩存結果全部清空,這也是一般不推薦使用查詢緩存的原因,如果這個表只會進行查詢操作,那可以使用。

  連接成功后,分析器會通過詞法和語法解析知道這是一條更新語句,優化器決定使用ID這個索引,然后執行器找到這一行,對數據進行更新。與查詢流程不一樣的是,更新流程涉及到了兩個重要的日志模塊,分別是redo log(重做日志)binlog(歸檔日志),這兩個模塊也就是支撐我們可以對MySQL數據進行恢復的基石,就好像我們可以使MySQL恢復到半個月內任意一秒的狀態。

三、日志模塊

 1、redo log重做日志

  官方的解釋說明:The redo log is a disk-based data structure used during crash recovery to correct data written by incomplete transactions. During normal operations, the redo log encodes requests to change table data that result from SQL statements or low-level API calls. Modifications that did not finish updating the data files before an unexpected shutdown are replayed automatically during initialization, and before the connections are accepted.重做日志是基於磁盤的數據結構,在崩潰恢復期間用於糾正不完整事務寫入的數據。在正常操作期間,重做日志對改變表數據的SQL的預處理請求或低級API的調用請求進行編碼。在初始化期間或接受連接之前,會自動執行在意外關閉之前而未完成的數據文件更新的修改。也就是說當在對數據文件進行更新時,因意外導致MySQL停止服務,在重啟MySQL后會自動繼續關閉前的操作。

  有這樣一個故事,一個開酒店的掌櫃他有一個粉板,專門用來記錄客人的賒賬記錄。如果賒賬的人不多,那么可以把全部的賒賬人名和賬目寫在粉板上,但如果賒賬的人太多了,粉板寫不下去,那他需要一個專門記錄賒賬的賬本。如果有人要賒賬或還賬,掌櫃有兩種做法:

  • 一種是把賬本翻出來找到對應的記錄,把這次賒的賬加上去或扣除掉;
  • 另一種是先在粉板上記錄下來,等打烊后把賬本翻出來核算處理;

  如果是在生意紅火比如午餐或晚餐時間,掌櫃的一定選擇第二種,比較方便,因為第一種太麻煩了,需要一個個去賬本翻,太費時間了。同樣對於MySQL來說,如果每次更新操作都寫入磁盤,然后磁盤查找對應的記錄,然后再更新,整個過程IO成本、查找成本、時間成本都很高。為了解決這個問題,MySQL設計者就引入了類似粉板的思路來提升更新效率。

   粉板和賬本配合的整個過程,就是MySQL的WAL(Write-Ahead Logging),即先寫日志,再寫磁盤。具體來說,當一條記錄需要更新時,InnoDB引擎會先記錄寫到redo log(粉板)里面,並更新內存,這個時候更新就完成了。同時InnoDB引擎會在適當的時候,將這個操作記錄更新到磁盤里面,而這個更新往往是在系統比較空閑的時候做。

  如果今天賒賬的不多,可以等到空閑的時候整理,但如果賒賬太多,粉板寫滿了,這個時候掌櫃只能停下手中的活,把粉板中的一部分記錄更新到賬本中,然后把這些記錄從粉板中擦除,為新記錄騰出空間。於此類似,InnoDB的redo log是固定大小的,比如可以配置為一組4個文件,每個文件的大小是1GB,那么這個”粉板“總共就可以記錄4GB的操作。從頭開始寫,寫到末尾就又回到開頭循環寫,如圖所示:

  write pos是當前記錄的位置,一邊寫一邊后移,寫到第3號文件(ib-logfile-3)末尾后就回到0號文件開頭。check point是當前要擦除的位置,也是往后推移並且循環的,擦除記錄前要把記錄更新到數據文件。

  write pos和check point之間的是”粉板“還空着的部分,表示可以用來記錄新的操作。如果write pos追上check point,表示”粉板“滿了,這時候不能再執行新的更新,而是停下來先擦掉一些記錄,把check point推進一下。

  有了redo log后,InnoDB就可以保證即使數據庫發生異常重啟,之前提交的記錄都不會丟失,這個能力稱為crash-safe。

2、binlog重要的日志模塊

  前面說過,MySQL整體有兩塊:一塊是Server層做MySQL功能層面的事情,另一塊是引擎層,負責存儲相關的事情,上面的redo log是InnoDB引擎特有的日志,而Server層也有自己的日志,稱為binlog(歸檔日志).

  你一定奇怪為什么會有兩份日志呢?因為最開始MySQL里並沒有InnoDB引擎,MySQL自帶的引擎是MyISAM,但是MyISAM沒有crash-safe能力,binlog日志只能用於歸檔,而InnoDB是以插件形式引入MySQL的,既然只依靠binlog是沒有crash-safe能力的,所以InnoDB使用另外一套日志系統--redo log來實現crash-safe能力。

  這兩種日志有以下的區別:

  • redo log是InnoDB引擎特有的;binlog是MySQL的Server層實現的,所有引擎都可以使用;
  • redo log是物理日志,記錄的是”在某個數據頁上做了什么修改“;binlog是邏輯日志,記錄的是這個語句的原始邏輯,比如”給ID=2這一行的c字段加1“;
  • redo log是循環寫的,空間固定會用完;binlog是可以追加寫入的。”追加寫“是指binlog文件寫到一定大小后會切換到下一個,並不會覆蓋以前的日志;

  有了對這兩個日志的理解,我們再來看看執行器和InnoDB引擎在執行這個簡單的update語句時的內部流程:

  1. 執行器先找引擎取ID=2這一行,ID是主鍵,引擎直接用樹搜索找到這一行。如果ID=2這一行所在的數據頁本來就在內存中,就直接返回給執行器;否則,需要從磁盤中讀入到內存,再返回;
  2. 執行器拿到引擎給的行數據,把這個值加上1,比如原來是N,現在就是N+1,得到新的一行數據,再調用引擎接口寫入這行新數據;
  3. 引擎將這行新數據更新到內存中,同時將這個更新操作記錄到redo log里面,此時redo log處於prepare狀態,然后告知執行器執行完成了,隨時可以提交事務;
  4. 執行器生成這個操作的binlog,並把binlog寫入磁盤;
  5. 執行器調用引擎的提交事務接口,引擎把剛剛寫入的redo log改成提交(commit)狀態,更新完成。

  下面是update語句的執行流程圖,圖中淺色框表示是在InnoDB內部執行的,深色框表示是在執行器中執行的:

 

  你可能注意到了,最后三步看上去有點“繞”,將redo log的寫入拆成了兩個步驟:prepare和commit,這其實就是“兩階段提交”。

兩階段提交

  為什么必須有“兩階段提交”呢?這是為了讓兩份日志之間的邏輯一致。要說明這個問題,需要從前面一個問題說起:前面說過可以使MySQL恢復到半個月內任意一秒的狀態,這是怎么做到的呢?

  前面說過,binlog會記錄所有的邏輯操作,並且采用“追加寫”的形式,如果你的MySQL可以半個月內恢復,那么備份系統中一定會保存最近半個月的所有binlog,同時系統會定期做整庫備份。這里的“定期”取決於系統的重要性,可以是一天一備,頁可以是一周一備。

  當需要恢復到指定的某一秒時,比如某天下午兩點發現中午十二點有一次誤刪表,需要找回數據,那么你可以這么做:

  1. 首先 ,找到最近的一次全量備份,然后從這個備份恢復到臨時庫;
  2. 然后,從備份的時間點開始,將備份的binlog依次取出來,重放到中午誤刪表之前的那個時刻。

  這樣你的臨時庫就跟誤刪之前的線上庫一樣了,然后你可以把表數據從臨時庫取出來,按需要恢復到線上庫去。

  那么為什么日志需要“兩階段提交”呢?我們從反證法來進行解釋:由於redo log和binlog是兩個獨立的邏輯,如果不用兩階段提交,要么就是先寫完redo log再寫binlog,或者采用反過來的順序。我們來看看這兩種方式會有什么問題。

  仍然用前面的update語句來做例子。假設當前ID=2的行,字段c的值是0,再假設執行update語句過程中在寫完第一個日志后,第二個日志還沒有寫完期間發生了crash,會出現什么情況呢?

  1. 先寫redo log后寫binlog。假設在redo log寫完,binlog還沒有寫完的時候,MySQL進程異常重啟。由於前面說過的redo log寫完之后,系統即使崩潰,仍然能夠把數據恢復回來,所以恢復后這一行c的值是1.但是由於binlog沒寫完就crash了,這時binlog里面就沒有記錄這個語句。因此,之后備份日志時,存起來的binlog里面就沒有這條語句。然后你會發現,如果需要用這個binlog來恢復臨時庫的話,由於這個語句的binlog丟失,導致臨時庫少了一次更新,恢復回來的這一行c的值是0,跟原庫的值不一致。
  2. 先寫binlog后寫redo log。如果在binlog寫完之后crash,由於redo log還沒寫,崩潰恢復以后這個事務無效,所以這一行c的值是0。但是binlog里面已經記錄了“把c從0改成1”這個日志。所以,在之后用binlog來恢復的時候就多了一個事務出來,恢復出來的這一行c的值是1,與原庫的值不一致。

  可以看到,如果沒有“兩階段提交”,那么數據庫的狀態就有可能和用它的日志恢復出來的庫的狀態不一致。你可能會說,這個概率是不是很低,平時也沒有什么動不動就需要恢復臨時庫的場景呀?

  其實不是的,不只是誤操作后需要用這個過程來恢復數據,當你需要擴容的時候,也就是需要再多搭建一些備庫來增加系統的讀能力的時候,現在常見的做法就是用全量備份加上應用binlog來實現的,這個“不一致”就會導致你的主從數據庫不一致的情況。

  簡單來說,redo log和binlog都可以用於表示事務的提交狀態,而兩階段提交就是讓這兩個狀態保持邏輯上的一致。

  redo log用於保證crash-safe能力,innodb_flush_log_at_trx_commit這個參數設置成1是,表示每次事務的redo log都直接持久化到磁盤。這個參數建議設置成1,這樣可以保證MySQL異常重啟后數據不丟失。

  sync_binlog這個參數設置為1時,表示每次事務的binlog都持久化到磁盤,這個參數也建議設置為1,這樣可以保證MySQL異常重啟后binlog不丟失。

討論:

定期全量備份的周期“取決於系統重要性,有的是一天一備,有的是一周一備”。那么在什么場景下,一天一備會比一周一備更有優勢呢?或者說,它影響了這個數據庫系統的哪個指標?

答案:

好處是“最長恢復時間”更短。

在一天一備的模式里,最壞情況下需要應用一天的 binlog。比如,你每天 0 點做一次全量備份,而要恢復出一個到昨天晚上 23 點的備份。系統的對應指標就是 RTO(恢復目標時間)。當然這個是有成本的,因為更頻繁全量備份需要消耗更多存儲空間,所以這個 RTO 是成本換來的,就需要你根據業務重要性來評估了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM