前面我們系統瞭解了一個查詢語句的執行流程,並介紹了執行過程中涉及的處理模組。相信你還記得,一條查詢語句的執行過程一般是經過聯結器、分析器、優化器、執行器等功能模組,最後到達儲存引擎。
那麼一條更新語句的執行流程又是怎麼樣的呢?
有時候你聽DBA的同事說,MySQL可以恢復到半個月內任意一秒的狀態,有沒有產生過好奇, 這是怎麼做到的呢?
我們帶著這兩個問題往下看:
(MySQL 邏輯架構圖)
我們把MySQL的基本執行鏈路在拿過來進行看下, 可以確定的是, 查詢的那一套流程, 更新語句也會走一遍。
*在一個表上進行更新的時候,跟這個表有關的所有快取,都會失效,這條語句會把表上的所有快取結果都清空,所以我們建議不查詢快取的原因。
*
*接下來分析器會知道這是一條更新語句,如果更新的欄位有索引,優化器會採用這個欄位的索引,執行器具體負責執行,找到這一行資料,然後更新。
*
與查詢不一樣的是,更新流程會有兩個重要的日誌模組。redo log (重做日誌) 和 binlog (歸檔日誌)。
重要的日誌模組
一、redo log
在講redo log 之前, 我們要來回顧一篇文章《孔乙己》。
在這篇文章裡,有一個酒店掌櫃,有一塊粉板,專門用來記錄客人賒賬的記錄。如果賒賬的人不多,掌櫃可以把姓名和金額記錄到粉板上。但是如果人多了,粉板記不下了,掌櫃就要放下手頭的工作,記錄到專門的賬本上。
如果有人賒賬,掌櫃一般有兩種做法:
1、直接翻出賬本,進行記錄賒賬人和金額
2、先記錄到粉板,在不忙的時候記錄到賬本。
在生意紅火的時候,掌櫃會選擇後者,因為前者過於麻煩,效率太低。
在MySQL中,MySQL的設計者們,也採用了粉板的思路來提升效率,而粉板與賬本的配合過程中,其實就是MySQL中的WAL技術(write-ahead-logging),關鍵點就是 先寫日誌,再寫磁碟。也就是先寫粉板,再寫賬本。
也就是說,當一條更新語句過來時,InnoDB引擎會把記錄記錄到redo log(粉板)中,並更新記憶體,這個時候就算更新完成了。InnoDB引擎會在合適的時候,進行更新到磁碟中。
redo log 的大小是固定的,比如可以一組配置4個檔案,每個檔案大小是1G,那麼這個redo log 可以記錄4G。從頭開始寫,寫到末尾就又回到開頭迴圈寫,如下面這個圖所示。
(寫redo log(粉板) 示意圖 –圖引自 MySQL45講)
write pos 是當前記錄的位置,一邊寫一邊後移,寫到第 3 號檔案末尾後就回到 0 號檔案開頭。checkpoint 是當前要擦除的位置,也是往後推移並且迴圈的,擦除記錄前要把記錄更新到資料檔案。
write pos 和 checkpoint 中間空著的部分,就是可以寫入的部分,如果write pos 追上了 checkpoint ,代表寫滿了,就需要停下擦除一部分,把checkpoint推進一下。
有了 redo log,InnoDB 就可以保證即使資料庫發生異常重啟,之前提交的記錄都不會丟失,這個能力稱為 crash-safe。redo log 是屬於 InnoDB特有的日誌。
二、binlog
我們之前學習知道,MySQL有兩塊,一塊是server層,一塊是引擎層。也知道,redo log 是InnoDB特有的日誌,server層也有自己的日誌,就是binlog(歸檔日誌)。
為什麼會有兩份日誌呢?
因為在最開始,MySQL並沒有innodb引擎。MySQL自帶的是MyISAM,但是 MyISAM 沒有 crash-safe 的能力,binlog 日誌只能用於歸檔。而 InnoDB 是另一個公司以外掛形式引入 MySQL 的,既然只依靠 binlog 是沒有 crash-safe 能力的,所以 InnoDB 使用另外一套日誌系統——也就是 redo log 來實現 crash-safe 能力。
這兩種日誌有以下三點不同。
1、redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 層實現的,所有引擎都可以使用。
2、redo log 是物理日誌,記錄的是“在某個資料頁上做了什麼修改”;binlog 是邏輯日誌,記錄的是這個語句的原始邏輯,比如“給 ID=2 這一行的 c 欄位加 1 ”。
3、redo log 是迴圈寫的,空間固定會用完;binlog 是可以追加寫入的。“追加寫”是指 binlog 檔案寫到一定大小後會切換到下一個,並不會覆蓋以前的日誌。
有了對這兩個日誌的一些概念性理解,我們應該能知道流程是如何進行執行的了。
(update 語句的執行流程圖 –圖引自 MySQL45講)
先找到引擎取出這一行,引擎通過B+ TREE找到這一行。如果這個資料頁本就在記憶體,直接返回。否則從磁碟讀入記憶體,然後返回。
執行器拿到引擎給的資料,做上邏輯操作,呼叫引擎介面寫入資料。
引擎將新資料更新到記憶體,同時將這個操作記錄到redo log 中,此時redo log 處於 prepare狀態。然後告知執行器執行完成,可以提交(commit)事務。
執行器生成這個操作的binlog,把binlog寫入磁碟。呼叫提交事務介面,把剛剛的binlog改成提交狀態。
二、將redo log拆為prepare 和 commit 兩階段引發的思考
為什麼要有兩階段提交啊?
這是為了讓兩份日誌之間保持邏輯一致。要說明這個問題,就要想到我們開始的那個問題,怎麼恢復到半個月內任意一秒鐘的資料?。
因為binlog會記錄所有的邏輯操作,並且採用 “追加寫”的方式。如果DBA承諾可以恢復,說明一定是備份了半個月的所有的binlog,同時系統會定期整庫備份。
1. 首先找個臨時庫,完全空的
2. 找最近的一份全量備份,全量備份的時間點比如為202-01-31 00:00:00
3. 從202-01-31 00:00:00開始找出到誤刪前的增量binlog日誌,(需要將其誤操作記錄刪除, 不然還是會刪除掉之前的記錄)並執行到臨時庫 這樣就保證了臨時庫和誤刪前的庫一致了。
說了這麼多,那為什麼需要兩階段提交呢?
由於 redo log 和 binlog 是兩個獨立的邏輯,如果不用兩階段提交,要麼就是先寫完 redo log 再寫 binlog,或者採用反過來的順序。我們看看這兩種方式會有什麼問題。
1、先寫 redo log 後寫 binlog。假設在 redo log 寫完,binlog 還沒有寫完的時候,MySQL 程式異常重啟。由於我們前面說過的,redo log 寫完之後,系統即使崩潰,仍然能夠把資料恢復回來,所以恢復後這一行 c 的值是 1。但是由於 binlog 沒寫完就 crash 了,這時候 binlog 裡面就沒有記錄這個語句。因此,之後備份日誌的時候,存起來的 binlog 裡面就沒有這條語句。然後你會發現,如果需要用這個 binlog 來恢復臨時庫的話,由於這個語句的 binlog 丟失,這個臨時庫就會少了這一次更新,恢復出來的這一行 c 的值就是 0,與原庫的值不同。
1、先寫 binlog 後寫 redo log。。如果在 binlog 寫完之後 crash,由於 redo log 還沒寫,崩潰恢復以後這個事務無效,所以這一行 c 的值是 0。但是 binlog 裡面已經記錄了“把 c 從 0 改成 1”這個日誌。所以,在之後用 binlog 來恢復的時候就多了一個事務出來,恢復出來的這一行 c 的值就是 1,與原庫的值不同。
可以看到,如果不使用“兩階段提交”,那麼資料庫的狀態就有可能和用它的日誌恢復出來的庫的狀態不一致。
簡單說,redo log 和 binlog 都可以用於表示事務的提交狀態,而兩階段提交就是讓這兩個狀態保持邏輯上的一致。
本作品採用《CC 協議》,轉載必須註明作者和本文連結