以前咱們瞭解了一條查詢語句的執行流程,並介紹了執行過程當中涉及的處理模塊。一條查詢語句的執行過程通常是通過鏈接器、分析器、優化器、執行器等功能模塊,最後到達存儲引擎。ios
那麼,一條 SQL 更新語句的執行流程又是怎樣的呢?算法
首先咱們建立一個表 T,主鍵爲 id,建立語句以下:sql
CREATE TABLE `T` ( `ID` int(11) NOT NULL, `c` int(11) DEFAULT NULL, PRIMARY KEY (`ID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
插入一條數據:數據庫
INSERT INTO T VALUES ('2', '1');
數組
若是要將 ID=2 這一行的 c 的值加 1,SQL 語句爲:緩存
UPDATE T SET c = c + 1 WHERE ID = 2;
架構
前面介紹過 SQL 語句基本的執行鏈路,這裏把那張圖拿過來。由於,更新語句一樣會走一遍查詢語句走的流程。app
其中,這兩種日誌默認在數據庫的 data 目錄下,redo log 是 ib_logfile0 格式的,binlog 是 xxx-bin.000001 格式的。async
接下來讓咱們分別去研究下日誌模塊中的 redo log 和 binlog。分佈式
在 MySQL 中,若是每一次的更新操做都須要寫進磁盤,而後磁盤也要找到對應的那條記錄,而後再更新,整個過程 IO 成本、查找成本都很高。爲了解決這個問題,MySQL 的設計者就採用了日誌(redo log)來提高更新效率。
而日誌和磁盤配合的整個過程,其實就是 MySQL 裏的 WAL 技術,WAL 的全稱是 Write-Ahead Logging,它的關鍵點就是先寫日誌,再寫磁盤。
具體來講,當有一條記錄須要更新的時候,InnoDB 引擎就會先把記錄寫到 redo log(redolog buffer)裏面,並更新內存(buffer pool),這個時候更新就算完成了。同時,InnoDB 引擎會在適當的時候(如系統空閒時),將這個操做記錄更新到磁盤裏面(刷髒頁)。
redo log 是 InnoDB 存儲引擎層的日誌,又稱重作日誌文件,redo log 是循環寫的,redo log 不是記錄數據頁更新以後的狀態,而是記錄這個頁作了什麼改動。
redo log 是固定大小的,好比能夠配置爲一組 4 個文件,每一個文件的大小是 1GB,那麼日誌總共就能夠記錄 4GB 的操做。從頭開始寫,寫到末尾就又回到開頭循環寫,以下圖所示。
圖中展現了一組 4 個文件的 redo log 日誌,checkpoint 是當前要擦除的位置,擦除記錄前須要先把對應的數據落盤(更新內存頁,等待刷髒頁)。write pos 到 checkpoint 之間的部分能夠用來記錄新的操做,若是 write pos 和 checkpoint 相遇,說明 redolog 已滿,這個時候數據庫中止進行數據庫更新語句的執行,轉而進行 redo log 日誌同步到磁盤中。checkpoint 到 write pos 之間的部分等待落盤(先更新內存頁,而後等待刷髒頁)。
有了 redo log 日誌,那麼在數據庫進行異常重啓的時候,能夠根據 redo log 日誌進行恢復,也就達到了 crash-safe。
redo log 用於保證 crash-safe 能力。innodb_flush_log_at_trx_commit 這個參數設置成 1 的時候,表示每次事務的 redo log 都直接持久化到磁盤。這個參數建議設置成 1,這樣能夠保證 MySQL 異常重啓以後數據不丟失。
MySQL 總體來看,其實就有兩塊:一塊是 Server 層,它主要作的是 MySQL 功能層面的事情;還有一塊是引擎層,負責存儲相關的具體事宜。redo log 是 InnoDB 引擎特有的日誌,而 Server 層也有本身的日誌,稱爲 binlog(歸檔日誌)。
binlog 屬於邏輯日誌,是以二進制的形式記錄的是這個語句的原始邏輯,依靠 binlog 是沒有 crash-safe 能力的。
binlog 有兩種模式,statement 格式的話是記 sql 語句,row 格式會記錄行的內容,記兩條,更新前和更新後都有。
sync_binlog 這個參數設置成 1 的時候,表示每次事務的 binlog 都持久化到磁盤。這個參數也建議設置成 1,這樣能夠保證 MySQL 異常重啓以後 binlog 不丟失。
爲何會有兩份日誌呢?
由於最開始 MySQL 裏並無 InnoDB 引擎。MySQL 自帶的引擎是 MyISAM,可是 MyISAM 沒有 crash-safe 的能力,binlog 日誌只能用於歸檔。而 InnoDB 是另外一個公司以插件形式引入 MySQL 的,既然只依靠 binlog 是沒有 crash-safe 能力的,因此 InnoDB 使用另一套日誌系統——也就是 redo log 來實現 crash-safe 能力。
redo log 和 binlog 區別:
有了對這兩個日誌的概念性理解後,再來看執行器和 InnoDB 引擎在執行這個 update 語句時的內部流程。
下圖爲 update 語句的執行流程圖,圖中灰色框表示是在 InnoDB 內部執行的,綠色框表示是在執行器中執行的。
其中將 redo log 的寫入拆成了兩個步驟:prepare 和 commit,這就是兩階段提交(2PC)。
MySQL 使用兩階段提交主要解決 binlog 和 redo log 的數據一致性的問題。
redo log 和 binlog 均可以用於表示事務的提交狀態,而兩階段提交就是讓這兩個狀態保持邏輯上的一致。下圖爲 MySQL 二階段提交簡圖:
兩階段提交原理描述:
備註: 每一個事務 binlog 的末尾,會記錄一個 XID event,標誌着事務是否提交成功,也就是說,recovery 過程當中,binlog 最後一個 XID event 以後的內容都應該被 purge。
binlog 會記錄全部的邏輯操做,而且是採用追加寫的形式。當須要恢復到指定的某一秒時,好比今天下午二點發現中午十二點有一次誤刪表,須要找回數據,那你能夠這麼作:
這樣你的臨時庫就跟誤刪以前的線上庫同樣了,而後你能夠把表數據從臨時庫取出來,按須要恢復到線上庫去。
redo log 和 binlog 有一個共同的數據字段,叫 XID。崩潰恢復的時候,會按順序掃描 redo log:
一個事務的 binlog 是有完整格式的:
在 MySQL 5.6.2 版本之後,還引入了 binlog-checksum 參數,用來驗證 binlog 內容的正確性。對於 binlog 日誌因爲磁盤緣由,可能會在日誌中間出錯的狀況,MySQL 能夠經過校驗 checksum 的結果來發現。因此,MySQL 是有辦法驗證事務 binlog 的完整性的。
redo log 過小的話,會致使很快就被寫滿,而後不得不強行刷 redo log,這樣 WAL 機制的能力就發揮不出來了。
若是是幾個 TB 的磁盤的話,直接將 redo log 設置爲 4 個文件,每一個文件 1GB。
實際上,redo log 並無記錄數據頁的完整數據,因此它並無能力本身去更新磁盤數據頁,也就不存在由 redo log 更新過去數據最終落盤的狀況。
在一個事務的更新過程當中,日誌是要寫屢次的。好比下面這個事務:
begin; INSERT INTO T1 VALUES ('1', '1'); INSERT INTO T2 VALUES ('1', '1'); commit;
這個事務要往兩個表中插入記錄,插入數據的過程當中,生成的日誌都得先保存起來,但又不能在還沒 commit 的時候就直接寫到 redo log 文件裏。
所以就須要 redo log buffer 出場了,它就是一塊內存,用來先存 redo 日誌的。也就是說,在執行第一個 insert 的時候,數據的內存被修改了,redo log buffer 也寫入了日誌。
可是,真正把日誌寫到 redo log 文件,是在執行 commit 語句的時候作的。
如下是我截取的部分 redo log buffer 的源代碼:
/** redo log buffer */ struct log_t{ char pad1[CACHE_LINE_SIZE]; lsn_t lsn; ulint buf_free; // buffer 內剩餘空間的起始點的 offset #ifndef UNIV_HOTBACKUP char pad2[CACHE_LINE_SIZE]; LogSysMutex mutex; LogSysMutex write_mutex; char pad3[CACHE_LINE_SIZE]; FlushOrderMutex log_flush_order_mutex; #endif /* !UNIV_HOTBACKUP */ byte* buf_ptr; // 隱性的 buffer byte* buf; // 真正操做的 buffer bool first_in_use; ulint buf_size; // buffer大小 bool check_flush_or_checkpoint; UT_LIST_BASE_NODE_T(log_group_t) log_groups; #ifndef UNIV_HOTBACKUP /** The fields involved in the log buffer flush @{ */ ulint buf_next_to_write; volatile bool is_extending; lsn_t write_lsn; /*!< last written lsn */ lsn_t current_flush_lsn; lsn_t flushed_to_disk_lsn; ulint n_pending_flushes; os_event_t flush_event; ulint n_log_ios; ulint n_log_ios_old; time_t last_printout_time; /** Fields involved in checkpoints @{ */ lsn_t log_group_capacity; lsn_t max_modified_age_async; lsn_t max_modified_age_sync; lsn_t max_checkpoint_age_async; lsn_t max_checkpoint_age; ib_uint64_t next_checkpoint_no; lsn_t last_checkpoint_lsn; lsn_t next_checkpoint_lsn; mtr_buf_t* append_on_checkpoint; ulint n_pending_checkpoint_writes; rw_lock_t checkpoint_lock; #endif /* !UNIV_HOTBACKUP */ byte* checkpoint_buf_ptr; byte* checkpoint_buf; /* @} */ };
redo log buffer 本質上只是一個 byte 數組,可是爲了維護這個 buffer 還須要設置不少其餘的 meta data,這些 meta data 所有封裝在 log_t 結構體中。
這篇文章主要介紹了 MySQL 裏面最重要的兩個日誌,即物理日誌 redo log(重作日誌)和邏輯日誌 binlog(歸檔日誌),還講解了有與日誌相關的一些問題。
另外還介紹了與 MySQL 日誌系統密切相關的兩階段提交(2PC),兩階段提交是解決分佈式系統的一致性問題經常使用的一個方案,相似的還有 三階段提交(3PC) 和 PAXOS 算法。
參考《MySQL實戰45講》