1. 程式人生 > >MySQL實戰 | 02-MySQL 如何恢復到半個月內任意一秒的狀態?

MySQL實戰 | 02-MySQL 如何恢復到半個月內任意一秒的狀態?

原文連結:MySQL是如何做到可以恢復到任意一秒狀態的?

看到這個題目是不是覺得資料庫再也不用擔心伺服器 crash 了?

那我們需要學習為什麼可以這麼做?以及如何做?

即為什麼可以恢復到任意時間點?如何恢復到任意時間點?

為什麼有了 binlog 還需要 redo log?

事務是如何提交的?事務提交先寫 binlog 還是 redo log?如何保證這兩部分的日誌做到順序一致性?

為了保障主從複製安全,故障恢復是如何做的?

上一次課我們學習了一條 select 語句的全部執行過程,那麼今天我們就從一條 update 語句開始。

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

其實執行流程和查詢流程一致,只是最後執行器執行的是找到這條資料,並進行更新。

另外,更新過程還涉及到一個重要的日誌模組,即 redo log(重做日誌)和 binlog(歸檔日誌)。

我個人是隻聽過 binlog 的。

redo log

和大多數關係型資料庫一樣,InnoDB 記錄了對資料檔案的物理更改,並保證總是日誌先行

也就是所謂的 WAL(Write-Ahead Logging),即在持久化資料檔案前,保證之前的 redo 日誌已經寫到磁碟。

MySQL 的每一次更新並沒有每次都寫入磁碟,InnoDB 引擎會先將記錄寫到 redo log 裡,並更新到記憶體中,然後再適當的時候,再把這個記錄更新到磁碟。

這裡有必要貼一下 InnoDB 的儲存結構圖:

[InnoDB 物理儲存結構][1]

如果下面看的各種空間懵逼了,建議回來看一眼這個圖。

redo log 是啥

當資料庫對資料做修改的時候,需要把資料頁從磁碟讀到 buffer pool 中,然後在 buffer pool 中進行修改,那麼這個時候 buffer pool 中的資料頁就與磁碟上的資料頁內容不一致,我們稱 buffer pool 的資料頁為 dirty page 髒資料

[dirty page][2]

這裡也可以看出,所有的更新操作都是現在 dirty page 中進行的。

如果這個時候發生非正常的 DB 服務重啟,那麼這些資料還沒在記憶體,並沒有同步到磁碟檔案中(注意,同步到磁碟檔案是個隨機 IO),也就是會發生資料丟失

如果這個時候,能夠在有一個檔案,當 buffer pool 中的 dirty page 變更結束後,把相應修改記錄記錄到這個檔案(注意,記錄日誌是順序 IO),那麼當 DB 服務發生 crash 的情況,恢復 DB 的時候,也可以根據這個檔案的記錄內容,重新應用到磁碟檔案,資料保持一致。

這個檔案就是 redo log ,用於記錄資料修改後的記錄,順序記錄。

我理解的,redo log 就是存放 dirty page 的物理空間。

log 何時產生 & 釋放?

在事務開始之後就產生 redo log,redo log 的落盤並不是隨著事務的提交才寫入的,而是在事務的執行過程中,便開始寫入 redo log 檔案中。

當對應事務的髒頁寫入到磁碟之後,redo log 的使命也就完成了,重做日誌佔用的空間就可以重用(被覆蓋)。

如何寫?

Redo log 檔案以 ib_logfile[number] 命名,並以順序的方式寫入檔案檔案,寫滿時則回溯到第一個檔案,進行覆蓋寫。

[迴圈寫][3]

如圖所示:

  • write pos 是當前記錄的位置,一邊寫一邊後移,寫到最後一個檔案末尾後就回到 0 號檔案開頭;
  • checkpoint 是當前要擦除的位置,也是往後推移並且迴圈的,擦除記錄前要把記錄更新到資料檔案;

write pos 和 checkpoint 之間還空著的部分,可以用來記錄新的操作。

如果 write pos 追上 checkpoint,表示寫滿,這時候不能再執行新的更新,得停下來先擦掉一些記錄,把 checkpoint 推進一下。

Redo log 檔案是迴圈寫入的,在覆蓋寫之前,總是要保證對應的髒頁已經刷到了磁碟

在非常大的負載下,Redo log 可能產生的速度非常快,導致頻繁的刷髒操作,進而導致效能下降。

通常在未做 checkpoint 的日誌超過檔案總大小的 76% 之後,InnoDB 認為這可能是個不安全的點,會強制的 preflush 髒頁,導致大量使用者執行緒 stall 住。

如果可預期會有這樣的場景,我們建議調大 redo log 檔案的大小。可以做一次乾淨的 shutdown,然後修改 Redo log 配置,重啟例項。

參考:
[http://mysql.taobao.org/monthly/2015/05/01/][4]

相關配置

預設情況下,對應的物理檔案位於資料庫的 data 目錄下的 ib_logfile1ib_logfile2

innodb_log_group_home_dir 指定日誌檔案組所在的路徑,預設./ ,表示在資料庫的資料目錄下。
innodb_log_files_in_group 指定重做日誌檔案組中檔案的數量,預設2
# 關於檔案的大小和數量,由一下兩個引數配置
innodb_log_file_size 重做日誌檔案的大小。
innodb_mirrored_log_groups 指定了日誌映象檔案組的數量,預設1

其他

redo log 有一個快取區 Innodb_log_buffer,預設大小為 8M,Innodb 儲存引擎先將重做日誌寫入 innodb_log_buffer 中。

[寫 redo log 過程][5]

然後會通過以下三種方式將 innodb 日誌緩衝區的日誌重新整理到磁碟:

1、Master Thread 每秒一次執行重新整理 Innodb_log_buffer 到重做日誌檔案;
2、每個事務提交時會將重做日誌重新整理到重做日誌檔案;
3、當 redo log 快取可用空間少於一半時,重做日誌快取被重新整理到重做日誌檔案;

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

CrashSafe 能夠保證 MySQL 伺服器宕機重啟後:

  • 所有已經提交的事務的資料仍然存在。
  • 所有沒有提交的事務的資料自動回滾。

binlog

如前文所講,MySQL 整體可以分為 Server 層和引擎層。

其實,redo log 是屬於引擎層的 InnoDB 所特有的日誌,而 Server 層也有自己的日誌,即 binlog(歸檔日誌)。

記錄了什麼

邏輯格式的日誌,可以簡單認為就是執行過的事務中的 sql 語句。

但又不完全是 sql 語句這麼簡單,而是包括了執行的 sql 語句(增刪改)反向的資訊。

也就意味著 delete 對應著 delete 本身和其反向的 insert;update 對應著 update 執行前後的版本的資訊;insert 對應著 delete 和 insert 本身的資訊。

何時產生 & 釋放

事務提交的時候,一次性將事務中的 sql 語句按照一定的格式記錄到 binlog 中。因此,對於較大事務的提交,可能會變得比較慢一些。

binlog 的預設是保持時間由引數 expire_logs_days 配置,也就是說對於非活動的日誌檔案,在生成時間超過配置的天數之後,會被自動刪除。

區別

1、redo log 是 InnoDB 引擎特有的,binlog 是 MySQL 的 Server 層實現,所有引擎都可以使用;
2、內容不同:redo log 是物理日誌,記錄的是在資料頁上做了什麼修改,是正在執行中的 dml 以及 ddl 語句;而 binlog 是邏輯日誌,記錄的是語句的原始邏輯,已經提交完畢之後的 dml 以及 ddl sql 語句,如「給 ID=2 的這一行的 c 欄位加 1」;
3、寫方式不同:redo log 是迴圈寫的,空間固定;binlog 是可以一直追加寫的,一個檔案寫到一定大小後,會繼續寫下一個,之前寫的檔案不會被覆蓋;
4、作用不同:redo log 主要用來保證事務安全,作為異常 down 機或者介質故障後的資料恢復使用,binlog 主要用來做主從複製和即時點恢復時使用;
5、另外,兩者日誌產生的時間,可以釋放的時間,在可釋放的情況下清理機制,都是完全不同的。

參考:
[http://www.importnew.com/28039.html][6]


資料更新事務流程

有了對這兩個日誌的概念性理解,我們再來看執行器和 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)狀態,更新完成。

[事務流程][7]

兩階段提交

上面處理 redo log 和 binlog 看著是不是有點懵逼?

其實這就是所謂的兩階段提交,即 COMMIT 會被自動的分成 prepare 和 commit 兩個階段。

[兩階段提交][8]

MySQL 在 prepare 階段會生成 xid,然後會在 commit 階段寫入到 binlog 中。在進行恢復時事務要提交還是回滾,是由 Binlog 來決定的。

由上面的二階段提交流程可以看出,通過兩階段提交方式保證了無論在任何情況下,事務要麼同時存在於儲存引擎和 binlog 中,要麼兩個裡面都不存在。

這樣就可以保證事務的 binlog 和 redo log 順序一致性。一旦階段 2 中持久化 Binlog 完成,就確保了事務的提交。

此外需要注意的是,每個階段都需要進行一次 fsync 操作才能保證上下兩層資料的一致性。

PS:記錄 Binlog 是在 InnoDB 引擎 Prepare(即 Redo Log 寫入磁碟)之後,這點至關重要。
另外需要注意的一點就是,SQL 語句產生的 Redo 日誌會一直重新整理到磁碟(master thread 每秒 fsync redo log),而 Binlog 是事務 commit 時才重新整理到磁碟,如果 binlog 太大則 commit 時會慢。

參考:
[http://www.ywnds.com/?p=7892][9]

如何恢復資料?

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

1、首先,找到最近的一次全量備份,如果你運氣好,可能就是昨天晚上的一個備份,從這個備份恢復到臨時庫;

2、然後,從備份的時間點開始,將備份的 binlog 依次取出來,重放到中午誤刪表之前的那個時刻。

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

當遇到 crash 時,恢復的過程也非常簡單:

1、掃描最後一個 Binlog 檔案,提取其中的 xid;
2、重做檢查點以後的 redo 日誌,蒐集處於 prepare 階段的事務連結串列,將事務的 xid 與 binlog 中的 xid 對比,若存在,則提交,否則就回滾;

總結一下,基本頂多會出現下面是幾種情況:

  • 當事務在 prepare 階段 crash,資料庫 recovery 的時候該事務未寫入 Binary log 並且儲存引擎未提交,將該事務 rollback。
  • 當事務在 binlog 階段 crash,此時日誌還沒有成功寫入到磁碟中,啟動時會 rollback 此事務。
  • 當事務在 binlog 日誌已經 fsync 到磁碟後 crash,但是 InnoDB 沒有來得及 commit,此時 MySQL 資料庫 recovery 的時候將會讀出 binlog 中的 xid,然後告訴 InnoDB 提交這些 xid 的事務,InnoDB 提交完這些事務後會回滾其它的事務,使儲存引擎和二進位制日誌始終保持一致。

總結起來說就是如果一個事務在 prepare 階段中落盤成功,並在 MySQL Server 層中的 binlog 也寫入成功,那這個事務必定 commit 成功。

總結

介紹了 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 日誌系統密切相關的「兩階段提交」。兩階段提交是跨系統維持資料邏輯一致性時常用的一個方案,即使你不做資料庫核心開發,日常開發中也有可能會用到。