MySQL日誌(一條sql更新語句是如何執行的)

kking_edc發表於2020-12-08

上一節我們系統瞭解了一個查詢語句的執行流程,並介紹了執行過程中涉及的處理模組。在這節,我們主要關心的是一條更新語句的指令執行流程。

還是從一個表的一個更新語句說起,下面是這個表的建立語句,這個表有一個主鍵 ID 和一個整型欄位 c:

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

如果要將 ID=2 這一行的值加 1,SQL 語句就會這麼寫:

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

再看下之前提到的sql執行鏈路圖(也就是MySQL架構圖),首先,可以確定的說,查詢語句的那一套流程,更新語句也是同樣會走一遍。
在這裡插入圖片描述
我們在執行語句前要先連線資料庫,這部分是聯結器的工作。

如果我們更新了一個表,在上一節中提到,那麼跟這個表有關的查詢快取會失效,所以這條語句就會把表 T 上所有快取結果都清空(這也就是我們一般不建議使用查詢快取的原因)。

接下來,分析器會通過詞法和語法解析知道這是一條更新語句。優化器決定要使用 ID 這個索引。然後,執行器負責具體執行,找到這一行,然後更新。

與查詢流程不一樣的是,更新流程還涉及兩個重要的日誌模組,它們正是我們今天要討論的主角:redo log(重做日誌)和 binlog(歸檔日誌)。如果接觸 MySQL,那這兩個詞肯定是繞不過的,在後面的內容裡我們也會不斷強調這兩個概念。同時有趣的是,redo log 和 binlog 在設計上有很多有意思的地方,這些設計思路也可以用到我們自己的程式裡。

一、redo log日誌模組

對於MySQL資料庫的更新來講,有這樣一個問題:如果每一次的更新操作都需要寫進磁碟,然後磁碟也要找到對應的那條記錄,然後再更新,整個過程 IO 成本、查詢成本都很高。為了解決這個問題,MySQL的設計者就使用了一種名為WAL(Write-Ahead Logging)的技術,它的關鍵點在於先寫日誌,再寫磁碟。

具體來說,當有一條記錄需要更新的時候,InnoDB 引擎就會先把記錄寫到 redo log裡面,並更新記憶體,這個時候更新就算完成了。同時,InnoDB 引擎會在適當的時候,將這個操作記錄更新到磁碟裡面,而這個更新往往是在系統比較空閒的時候做,這樣就提高了整個更新的效率。

redo log是固定大小的,比如可以配置為一組 4 個檔案,每個檔案的大小是 1GB,那麼整個redo log就可以記錄4GB的操作。它的組織形式是環狀的,如下所示:
在這裡插入圖片描述
write pos 是當前記錄的位置,一邊寫一邊順時針後移,寫到第 3 號檔案末尾後就相當於回到 0 號檔案開頭。checkpoint 是當前要更新回硬碟的操作的位置,也是往後推移並且迴圈的,一邊將記錄更新回硬碟一邊將checkpoint後移(因為操作已經寫回硬碟,不需要繼續留在redo log中)。

write pos 和 checkpoint 之間的是redo log中還空著的部分(也就是綠色標出的部分),可以用來記錄新的操作。如果 write pos 追上 checkpoint,表示redo log已滿,這時候不能再執行新的更新,得停下來將一部分記錄更新回硬碟,把 checkpoint 推進一下。

有了 redo log,InnoDB 就可以保證即使資料庫發生異常重啟,之前提交的記錄也都不會丟失,這個能力稱為 crash-safe。可以從兩方面理解crash-safe,一方面,不在redo log中的記錄已經更新回了硬碟,這就儲存在了硬碟中,不會丟失,而另一方面,在redo log中的記錄不會因資料庫的異常重啟而丟失,也就不會丟失我們之前執行的操作,因此,可以保證資料庫的crash-safe。

二、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 能力。

這兩種日誌有以下三種不同:

  1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 層實現的,所有引擎都可以使用。
  2. redo log 是物理日誌,記錄的是“在某個資料頁上做了什麼修改”;binlog 是邏輯日誌,記錄的是這個語句的原始邏輯,比如“給 ID=2 這一行的 c 欄位加 1 ”。
  3. 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,這兩個步驟稱為“兩階段提交”

兩階段提交:

為什麼必須有“兩階段提交”呢?這是為了讓兩份日誌之間的邏輯一致。要說明這個問題,我們可以從這樣一個問題說起:怎樣讓資料庫恢復到半個月內任意一秒的狀態?

前面提到,binlog 會記錄所有的邏輯操作,並且是採用“追加寫”的形式。如果可以恢復半個月內的資料,那麼備份系統中一定會儲存最近半個月的所有 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 都可以用於表示事務的提交狀態,而兩階段提交就是讓這兩個狀態保持邏輯上的一致。

相關文章