1. 程式人生 > 其它 >MySql 日誌詳解

MySql 日誌詳解

一、日誌介紹

  MySql 日誌共有錯誤日誌、查詢日誌等等幾個大類的日誌,其中比較重要的主要是三種日誌:二進位制日誌 binlog(歸檔日誌)、事務日誌 redolog(重做日誌)、undolog(回滾日誌)。每種日誌的作用不盡相同,下文將對這三種日誌進行詳細介紹。

二、 redo日誌

  我們可以想象一個場景,我們知道我們對資料庫進行增刪改的時候,其實是需要讀取磁碟中的頁,然後對頁進行修改後,修改後將頁寫回磁碟,但單純的以這種方式會造成下面兩個問題:

1、重新整理一個完成的資料頁太過浪費,有時候我們只需要修改其中的一個位元組,但是由於 InnoDB 是以頁為單位進行磁碟 I/O 的,也就是說在該事務提交的時候不得不將一個完整的頁面從記憶體中重新整理到磁碟,我們又知道,一個頁的大小是 16 K,如果修改一個位元組就要重新整理 16K 的資料到磁碟上,顯然太浪費

2、隨機 I/O 重新整理起來十分慢,一個事務可能涉及到多個不同的頁面,這些頁面可能不相鄰,這就意味著 Buffer Pool 可能會進行很多隨機 I/O。

 

如何解決這兩個問題?

   我們的目的是想讓已經提交的事務對資料庫的修改能永久的生效,即使後來系統崩潰,在重啟後也能把這種修改恢復過來,所以其實沒有必要在每次事務提交的時候就把所有修改的全部頁面刷進磁碟,只需要把這個修改的內容記錄一下就好。

  比如:“將第 0 號表空間的 100 頁號中偏移量為 1000 處的值更新為 2”

  這樣在事務提交時,就會把上述內容刷進磁碟,這樣即使系統崩潰,重啟的時候更新一下資料頁,這樣資料庫的修改就能恢復過來,即保證了資料庫的 “永續性” —— 說過的話一定要做到。這樣的日誌稱為 redo 日誌。這樣做的好處:

1、日誌的儲存空間非常小,在儲存表空間 ID 、頁號、偏移量以及需要更新的值,

2、redo 日誌是按順序寫入磁碟的,在執行事務過程中,每執行一條語句,就可能產生若干條 redo 日誌,這些日誌是按照順序寫入磁碟,也就是使用順序 I/O

 

再思考一個問題:我們知道我們修改表會有很多種情況,比如修改索引,分頁等等,如果把這些一步一步的操作直接寫下來會非常大,那如何解決?

  這裡 InnoDB的解決辦法就是設定很多種類的日誌,我們在遇見不同種類的操作的時候,只需記得其引數(如頁號、偏移量等),在需要執行的時候,識別不同的日誌,呼叫不用的函式進行對應操作,而不是記錄具體操作步驟。

 

以組的形式的 redo 日誌

  InnoDB中日誌被劃分成許多不可分割的組,如:

  向聚簇索引對應的 B+ 樹的頁面插入一條記錄產生的 redo 是一組,是不可分割的

  向二級索引對應的 B+ 樹插入一條記錄產生的 redo 是一組,不可分割

 

  不可分割的指:日誌的操作必須是原子操作,我們在完成一項任務的時候,要就不做,要就全部做完,所以在每一個不可分割的塊中,都有開始和結束的標誌,一旦開始執行,就需要執行到結束標誌,否則丟棄前面的執行。

  而一個不可分割的過程稱為一個 Mini-Transaction(MTR),一個MTR包含若干個 redo 日誌。

 

 

redo 日誌的寫入過程

  • 日誌的儲存:為了更好的管理 redo 日誌,InnoDB設計出與頁相似的結構 —— redo log block, 一塊 512k 的儲存空間,把MTR全部都儲存在了這種小的儲存空間裡

 

那日誌直接是寫入磁碟嘛? 並不是,redo 日誌也有自己對應的日誌緩衝區。

在伺服器啟動的時候就向作業系統申請了一大塊稱為 redo log buffer 的連續記憶體空間,這片空間被劃分為若干個 redo log block

 

因此日誌不是直接就寫入磁碟,而是先寫入磁碟緩衝區,那什麼時候將緩衝區的日子刷入磁碟呢?有以下幾種情況

1、log buffer空間不足的時候,當前 log buffer佔用 50% 以上的時候,就會進行刷盤,將日誌刷進磁碟

2、事務提交的時候:之所以提出 redo 日誌的原因,就是其佔用的空間小,而且可以順序的寫入磁碟,引入 redo 日誌後,雖然在事務提交的時候可以不把修改過的 buffer pool 頁面立即刷入磁碟,但為了保證永續性,必須把頁面修改時對應的 redo 日誌重新整理進磁碟。

3、將某個髒頁刷進磁碟前,必須保證對應的 redo 日誌已經刷進磁碟(由於 redo 是順序重新整理的,所以在把髒頁的 redo 的刷進磁碟的前提就是,前面的日誌也全部刷近磁碟

4、後臺有個執行緒,大約每一秒會將 log buffer 中 redo 日誌刷進磁碟

5、正常關閉伺服器

6、做 checkpoint 時

 

什麼是 checkpoint ?

  由於我們的日誌檔案組的容量少有限的,所以不得不迴圈使用 redo 日誌檔案組中的檔案,但這會導致最後寫入的日誌 和 最先寫入的 日誌 衝突,所以 InnoDB 使用標誌標記現在已經刷到哪了,另一個標誌去標記新的日誌寫在哪,若即將發生衝突,則進行刷盤,防止覆蓋,這個過程稱為 checkpoint

 

其實 MySql 也提供了引數讓我們去選擇刷盤的策略:

InnoDB 儲存引擎為 redo log 的刷盤策略提供了 innodb_flush_log_at_trx_commit 引數,它支援三種策略:

  • 0 :設定為 0 的時候,表示每次事務提交時不進行刷盤操作
  • 1 :設定為 1 的時候,表示每次事務提交時都將進行刷盤操作(預設值)
  • 2 :設定為 2 的時候,表示每次事務提交時都只把 redo log buffer 內容寫入 page cache

innodb_flush_log_at_trx_commit 引數預設為 1 ,也就是說當事務提交時會呼叫 fsync 對 redo log 進行刷盤

另外,InnoDB 儲存引擎有一個後臺執行緒,每隔1 秒,就會把 redo log buffer 中的內容寫到檔案系統快取(page cache),然後呼叫 fsync 刷盤。

 

而資料庫會在合適的時候將 redo 日誌更新的內容重新整理進磁碟,這通常是在資料庫空閒的時候進行。

 

下面是 三種引數的刷盤策略

 

 

 

 

 二、binlog

  剛剛介紹的 redo 日誌是在 InnoDB 引擎上的日誌,而 binlog 則是在 Server 層進行操作的日誌:

  既然是在 Server 層的日誌,沒有區別引擎,說明所有引擎的資料庫都有 binlog 日誌,那為什麼 InnoDB 有一套日誌,MySql本身也有一套日誌呢?  

  因為以前的MySQL沒有InnoDB引擎,MySQL5.5前使用的 MyISAM引擎,但是 MyISAM 沒有 crash-safe 的能力,而 binlog 只能用於歸檔。InnoDB 是後來作為 MySQL 的引擎以外掛形式引入的。既然只靠 binlog 無法實現 crash-safe 的能力,所以 InnoDB 使用另一套日誌系統——redo log 來實現。   那我們利用一條 updata 語句的執行過程,來理解這兩個日誌分別行使的作用:假如我們要將 A 資料進行 +1 操作:
  1. 我們先查到對應資料 A 的主鍵 ID = 2,通過主鍵索引樹來找到這條資料所在的頁號。然後在 Buffer Pool 中進行查詢,如果查到,則直接操作,如果查不到,則從磁碟內進行讀取。
  2. 執行器拿到引擎給的這條資料,把它 +1 後,再呼叫引擎介面寫入這條資料。
  3. 引擎將這條資料更新至記憶體,同時記錄到 redo log,此時 redo log 處於 prepare 態,這就告訴執行器更新已經完成,隨時可以提交事務。
  4. 執行器生成這個操作的 binlog ,並把 binlog 寫入磁碟
  5. 執行器呼叫引擎的事務介面,引擎把剛剛的 redo log 改為提交狀態,這個事務執行完成  如下圖所示:

 

 

  為什麼 redo log有兩種狀態(prepare、commit)?即兩階段提交?

  為什麼要有兩階段提交,就是為了讓 redo log 和 binlog 兩個檔案保持一致。我們還是用反證法來說明,假設沒有兩階段提交會發生什麼問題:

  • 先寫 redo log,再寫 binlog,假設 redo log 寫完,binlog 還沒寫完,MySQL 程序異常重啟,redo log 寫完後,即使系統崩潰,仍然能把資料恢復回來,所有恢復後的資料是正確的。但是 binlog 沒寫完,這時候 binlog 中就沒有記錄這條語句的操作,因此,之後備份日誌的時候,binlog 就沒有這條操作記錄,如果用這個 binlog 來恢復臨時庫的話,由於這條語句記錄的丟失,臨時庫就會少了這一個語句的操作,恢復出來的資料就與原庫的值不同。
  • 先寫 binlog,再寫 redo log,如果在 binlog 寫完後系統崩潰了,由於 redo log 還沒寫,崩潰後這個事務無效,所以磁碟資料檔案中的資料是沒有這條語句的操作的,但是 binlog 中已經做了記錄,所以以後用這個 binlog 來做資料恢復時,就多了一個事務操作,與原庫的資料不一致。

如果沒有“兩階段提交”,會導致 redo log 和 binlog 記錄的操作不一致,那麼資料庫的狀態就有可能和用它的日誌恢復出來的庫資料不一致。

  所以,能夠保證 redo log 和 binlog 的操作記錄一致的流程是,將操作先更新到記憶體,再寫入 redo log,此時標記為 prepare 狀態,再寫入 binlog,此時再提交事務,將 redo log 標記為 commit 狀態。

 

那 binlog 的作用是什麼?如圖:

  

  

  binlog在MySQL的server層產生,不屬於任何引擎,主要記錄使用者對資料庫操作的SQL語句(除了查詢語句)。之所以將binlog稱為歸檔日誌,是因為binlog不會像redo log一樣擦掉之前的記錄迴圈寫,而是一直記錄(超過有效期才會被清理),如果超過單日誌的最大值(預設1G,可以通過變數 max_binlog_size 設定),則會新起一個檔案繼續記錄。但由於日誌可能是基於事務來記錄的(如InnoDB表型別),而事務是絕對不可能也不應該跨檔案記錄的,如果正好binlog日誌檔案達到了最大值但事務還沒有提交則不會切換新的檔案記錄,而是繼續增大日誌,所以 max_binlog_size 指定的值和實際的binlog日誌大小不一定相等。

正是由於binlog有歸檔的作用,所以binlog主要用作主從同步和資料庫基於時間點的還原。

那麼回到剛才的問題,binlog可以簡化掉嗎?這裡需要分場景來看:

  1. 如果是主從模式下,binlog是必須的,因為從庫的資料同步依賴的就是binlog;
  2. 如果是單機模式,並且不考慮資料庫基於時間點的還原,binlog就不是必須,因為有redo log就可以保證crash-safe能力了;但如果萬一需要回滾到某個時間點的狀態,這時候就無能為力,所以建議binlog還是一直開啟;

 

binlog 的寫入機制:binlog 同樣是先寫入到 binlog cache,事務提交的時候再把 binlog cache 的內容寫入磁碟,如果記憶體不足的時候可以利用 磁碟來進行 swap 儲存 。

 

 

binlog 和 redolog :

  • redo log 是InnoDB 引擎特有的;而 binlog 是MySQL Server 層實現的
  • redo log 是物理日誌,記錄的是“在某個資料頁做了什麼修改”;而 binlog 是邏輯日誌,記錄的是語句的原始邏輯。比如 update T set c=c+1 where ID=2;這條SQL,redo log 中記錄的是 :xx頁號,xx偏移量的資料修改為xxx;binlog 中記錄的是:id = 2 這一行的 c 欄位 +1
  • redo log 是迴圈寫的,固定空間會用完;binlog 可以追加寫入,一個檔案寫滿了會切換到下一個檔案寫,並不會覆蓋之前的記錄
  • 記錄內容時間不同,redo log 記錄事務發起後的 DML 和 DDL語句;binlog 記錄commit 完成後的 DML 語句和 DDL 語句
  • 作用不同,redo log 作為異常宕機或者介質故障後的資料恢復使用;binlog 作為恢復資料使用,主從複製搭建。

 

 

 講到這裡,我們來想一個完整的問題,如果 MySQL 在提交事務的時候突然崩潰,重啟的時候資料是如何恢復的?

   下面就是 MySQL 在重啟恢復後,在提供服務前需要做的事情:

 

 

 簡單來說共有兩步:

1、檢查已經刷盤的 redo 日誌是否已經更新進入資料庫,即日誌是否和資料一致

2、保證 redo log 和 binlog一致,奔潰重啟後會檢查redo log中是完整並且處於prepare狀態的事務,然後根據XID(事務ID),從binlog中找到對應的事務,如果找不到,則回滾,找到並且事務完整則重新commit redo log,完成事務的提交。

 

那這裡我們崩潰的時刻有幾種情況:

    1. 時刻A(剛在記憶體中更改完資料頁,還沒有開始寫redo log的時候奔潰):
      因為記憶體中的髒頁還沒刷盤,也沒有寫redo log和binlog,即這個事務還沒有開始提交,所以奔潰恢復跟該事務沒有關係;
    2. 時刻B(正在寫redo log或者已經寫完redo log並且落盤後,處於prepare狀態,還沒有開始寫binlog的時候奔潰):
      恢復後會判斷redo log的事務是不是完整的,如果不是則根據undo log回滾;如果是完整的並且是prepare狀態,則進一步判斷對應的事務binlog是不是完整的,如果不完整則一樣根據undo log進行回滾;
    3. 時刻C(正在寫binlog或者已經寫完binlog並且落盤了,還沒有開始commit redo log的時候奔潰):
      恢復後會跟時刻B一樣,先檢查redo log中是完整並且處於prepare狀態的事務,然後判斷對應的事務binlog是不是完整的,如果不完整則一樣根據undo log回滾,完整則重新commit redo log;
    4. 時刻D(正在commit redo log或者事務已經提交完的時候,還沒有反饋成功給客戶端的時候奔潰):
      恢復後跟時刻C基本一樣,都會對照redo log和binlog的事務完整性,來確認是回滾還是重新提交。

參考 : https://zhuanlan.zhihu.com/p/142491549

 

那這裡涉及到的 undo 日誌就是下面我要介紹的。

 

三、undo log

  前面說到用 redo 和 binlog 保證伺服器崩潰的時候資料的恢復,這樣可以保證已提交事務的永續性,但前面介紹過一種情況,在事務還沒有提交的時候,寫的 redo 日誌很可能已經刷盤,那麼這些未提交事務的修改過的頁在 MySql 重啟的時候可能也被恢復了,但為了保證事務的原子性 —— 要麼不做,要麼全部做完,這裡就可能會出現需要回滾資料的任務,這就要用到 undo 日誌 —— 做過的事情想反悔。

  undo 日誌記載了回滾一個操作所需的必要內容。而其原理設計到一個概念 : 事務 ID

  

事務 ID:

  在事務對表中記錄進行改動的時候(如 UpDate),才會為這個事務分配一個唯一的事務 ID,這個事務 ID 是一個遞增的數字,未被分配事務 ID 的事務預設為 0, 聚簇索引記錄中有一個 trx_id 隱藏列它代表對這個聚簇索引記錄進行改動的語句所在的事務對應的事務 ID。(在 undo 在 mvcc 中的應用中會用到)

 

 InnoDB 利用專門的頁面去儲存 undo 日誌,而在記錄中有著專門指向 undo 記錄的指標,如下圖所示:

 

 

 當需要回滾的時候,就會呼叫之前的 undo 記錄,進行資料的還原。