1叨逼叨
本文主要內容有:
-
redo log -
bin log -
WAL 技術 -
什么是crash-safe -
兩階段提交
2一條SQL更新語句是如何執行的?
上一篇:一條SQL查詢語句是如何執行的
一條查詢語句的流程一般經過連接器、分析器、優化器、執行器等模塊,最終到達存儲引擎。
那么問題來了,一條 sql 更新語句是怎么跑的?
以前可能聽到大佬或者運維的同事說,MySQL 可以恢復到半個月內任意一秒的狀態。

先建一個表
mysql> create table aaaqi_demo2(id int primary key ,c int);
Query OK, 0 rows affected (0.03 sec)

如果將 id 為 2 的這條記錄的 c 值更新為 4
mysql> update aaaqi_demo2 set c=4 where id=2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
之前查詢語句的流程圖如下,更新語句流程也是這樣走的
還是先連接數據庫走連接器。
現在是更新操作,和這個表相關的查詢緩存都會失效,這條語句會把 aaaqi_demo2 表上的所有緩存結果清空,這也是一般不建議使用查詢緩存的原因。當然像什么配置表之類還是可以的。
接下來走到分析器,分析器會通過詞法和語法分析知道這是一條更新語句,優化器會決定使用這個 id 的索引(其實這里優化器不一定使用你期望的索引,聽大佬說內部還有個投票選舉的機制,會使用票數多的那個索引,后面我會寫個demo)。
然后執行器負責具體的執行,找到這一行,然后更新。
更新和查詢不一樣的地方在於,它還涉及兩個重要的日志模塊:redo log(重做日志)
,bin log(歸檔日志)
。 這個兩個日志非常重要,MySQL 很多牛皮的功能都會用到。
3重要的日志模塊 redo log
這個日志的話,MySQL 有兩種做法:
-
先把記錄寫到 redo log 里面,等系統空閑的時候再更新到磁盤里面去。 -
每次的更新操作都需要寫進磁盤,然后磁盤也要找到對應的那條記錄,然后更新。
但是第2種做法會導致整個過程IO成本、查找成本都會很高。
為了解決這個問題,MySQL設計者用磁盤和rode log進行配合,這個過程就是 MySQL 里面常說的WAL 技術
,WAL 的全稱是“Write-Ahead Logging”,它的關鍵點就是先寫日志,再寫磁盤。
詳細點就是說當有一條記錄需要更新的時候,Innodb引擎就會先把記錄寫到redo log里面,並更新內存,這個時候更新就算完成了。同時,Innodb 引擎會在適當的時候把這個操作記錄更新到磁盤里面,而這個更新一般會在系統比較空閑的時候去做。
另外,Innodb的 redo log是固定大小的,比如一組可以配置4 個文件,每個文件大小是 1GB,那么總共就可以記錄 4GB 的操作。從頭開始寫,寫到末尾又回到開頭循環寫,如下圖:
write pos 是當前記錄位置,一邊寫一邊順時針向后移動,寫到 3 號文件末尾就回到 0 號文件開頭。checkpoint 是當前要擦除的位置,也是順時針往后移動並且循環的,擦除記錄前要把記錄更新到數據文件。
write pos 和 checkpoint 之間空着的部分用來記錄新的操作。如果 write pos 追上 checkpoint 那么表示redo log 滿了,這時候不能再執行更新操作,得先停下來擦掉一些記錄,把 checkpoint推進一些。
有了 redo log,Innodb就可以保證即使數據庫發生異常重啟,之前提交的記錄都不會丟失,這個能力稱為crash-safe
。
4重要日志模塊 binlog
MySQL 整體來看分為兩塊,一塊是server 層,一塊是引擎層。server層主要做 MySQL 功能方面的事情;引擎層主要負責存儲相關事宜。redo log 是Innodb引擎特有的日志,binlog 是 server 層的日志。
最開始MySQL並沒有Innodb引擎,默認是 MyISAM,但是 MyISAM 沒有crash-safe的能力,binlog 只能用於歸檔。而Innodb是另一個公司以插件的形式引入 MySQL 的,只依靠 binlog 是沒有 crash-safe 能力的,所以Innodb使用 redo-log 來實現crash-safe 能力。
redo-log、binlog 有幾點不同:
-
redo log 是Innodb引擎特有的;binlog 是MySQL server 層實現的,所有引擎都能使用。 -
redo log 是物理日志,記錄的是在某數據頁上做了什么修改;binlog 是邏輯日志,記錄的是這個語句的原始邏輯,比如:給 id 為 2 的這條數據c 字段更新為4。 -
redo log 是循環寫,固定空間會用完;binlog 是追加寫入的。“追加寫”是指binlog 文件寫到一定大小后會切換到下一個,並不會覆蓋以前的日志。
通過上面了解后,再看 Innodb 引擎執行之前簡單的 update 語句時,MySQL 內部是怎么走的。
-
執行器先找引擎取id=2 這行。id 是主鍵,引擎直接用樹搜索找到這一行。如果 id=2這行數據本來就在數據頁中的話,就直接返回給執行器;否則先要從磁盤讀入內存,然后再返回。
-
執行器拿到引擎給的行數據,原來是 2,現在更新為 4,得到新的一行數據,再調用引擎接口寫入這行新數據。
-
引擎將這行新數據更新到內存中,同時將這個更新操作記錄到 redo log 里面,此時 redo log 處於prepare 狀態。告知執行器執行完了,可以隨時提交事務。
-
執行器生成這個操作的 binlog,並把binlog 寫入磁盤。
-
執行器調用引擎的提交事務接口,引擎把剛剛寫入的 redo log改成 commit 狀態,更新完成。
下面是執行流程圖:

注意到最后三步了吧,將 redo log的寫入拆成兩個步驟:prepare 和 commit 這就是二階段提交
。
5二階段提交
怎樣讓數據庫恢復到半個月內任意一秒的狀態? 來說二階段提交。
前面提到過 binlog會記錄所有邏輯操作,並采用“追加寫”的形式。如果運維說一個月內可以恢復,那么備份系統中一定會保存最近一個月的所有 binlog,同時系統還會定期做整庫備份。
當需要恢復到某一秒時,比如某天下午兩點發現中午十二點有一次誤刪表,需要找回數據(如果是用的阿里的 MySQL,直接到那個后台管理,是可以直接恢復的),那么可以這樣做:
-
首先找到最近的一次全量備份,如果你運氣好,可能就是昨晚的一個備份,從這個備份恢復到臨時庫;
-
然后,從備份的時間點開始,將備份的binlog一次取出來,重放到中午誤刪表之前的那個時刻。
現在臨時庫就和之前誤刪之前一樣了,然后把表數據從臨時庫中取出按需恢復到線上庫。
6反證法 日志為什么需要二階段提交
由於redo log 和binlog是兩個獨立的邏輯,如果不用兩階段提交,要么就是先寫 binlog再寫 redo log,或者反過來寫。會出現什么問題。
前面寫的 update 舉例,假設 id=2 的這條數據,字段 c=0,再假設執行 update語句中執行完第一個日志后,第二個日志還沒寫完,期間發生了crash,會出現什么情況?
-
先寫 redo log后寫 binlog
。如果在 redo log 寫完,binlog 還沒寫完時,MySQL進程異常死掉了重啟。前面提到 redo log 寫完后,系統即使掛了,仍然能夠把數據恢復回來,所以恢復這行數據c 的值是 1。
由於binlog 沒寫完就crash 了,這時binlog 里面沒有記錄上這個語句。因此,后面備份日志的時候,存起來的 binlog里就沒有這條語句。
然后你就會發現,如果用這個 binlog 恢復臨時庫的話,由於這個語句的 binlog 丟失,臨時庫就少了這次更新,恢復出來的這行數據 c 值等於 0和原庫的值不同。
-
先寫 binlog 后寫 redo log
。如果在 binlog 寫完后 crash,由於redo log 么寫完,崩潰恢復以后這個事務無效,所以這行 c=0。但 binlog里已經記錄了把“c 從 2改成 4”這個日志。所以 binlog 恢復出來就會多出一個事務,恢復出的這一行c 的值是 4,與原庫的 c 值不同。
上面可以看到,如果不使用二階段提交,那么數據庫的狀態就有可能和它通過日志恢復的臨時庫的狀態不一致。
7相關配置
MySQL 里面最重要的兩個日志,即物理日志 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 不丟失。
不過我這個版本默認都是開啟的,其它版本不清楚:
mysql> SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
mysql> SHOW VARIABLES LIKE 'sync_binlog';

