一、日志介紹
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本身也有一套日志呢?
- 我們先查到對應數據 A 的主鍵 ID = 2,通過主鍵索引樹來找到這條數據所在的頁號。然后在 Buffer Pool 中進行查詢,如果查到,則直接操作,如果查不到,則從磁盤內進行讀取。
- 執行器拿到引擎給的這條數據,把它 +1 后,再調用引擎接口寫入這條數據。
- 引擎將這條數據更新至內存,同時記錄到 redo log,此時 redo log 處於 prepare 態,這就告訴執行器更新已經完成,隨時可以提交事務。
- 執行器生成這個操作的 binlog ,並把 binlog 寫入磁盤
- 執行器調用引擎的事務接口,引擎把剛剛的 redo log 改為提交狀態,這個事務執行完成 如下圖所示:
為什么要有兩階段提交,就是為了讓 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可以簡化掉嗎?這里需要分場景來看:
- 如果是主從模式下,binlog是必須的,因為從庫的數據同步依賴的就是binlog;
- 如果是單機模式,並且不考慮數據庫基於時間點的還原,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,完成事務的提交。
那這里我們崩潰的時刻有幾種情況:
- 時刻A(剛在內存中更改完數據頁,還沒有開始寫redo log的時候奔潰):
因為內存中的臟頁還沒刷盤,也沒有寫redo log和binlog,即這個事務還沒有開始提交,所以奔潰恢復跟該事務沒有關系; - 時刻B(正在寫redo log或者已經寫完redo log並且落盤后,處於prepare狀態,還沒有開始寫binlog的時候奔潰):
恢復后會判斷redo log的事務是不是完整的,如果不是則根據undo log回滾;如果是完整的並且是prepare狀態,則進一步判斷對應的事務binlog是不是完整的,如果不完整則一樣根據undo log進行回滾; - 時刻C(正在寫binlog或者已經寫完binlog並且落盤了,還沒有開始commit redo log的時候奔潰):
恢復后會跟時刻B一樣,先檢查redo log中是完整並且處於prepare狀態的事務,然后判斷對應的事務binlog是不是完整的,如果不完整則一樣根據undo log回滾,完整則重新commit redo log; - 時刻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 記錄,進行數據的還原。