簡介:日志是MySQL數據庫的重要組成部分,記錄著數據庫運行期間各種狀態信息,主要包括錯誤日志、查詢日志、慢查詢日志、事務日志、二進制日志幾大類。在此重點關注二進制日志bin log和事務日志(包括redo log?和?undo log?)。
一、概念
redo log、undo log&bin log
重做日志(redo log)、回滾日志(undo log)、二進制日志(binlog)的基本概念。
redo log 是物理日志,undo log 和 binlog 是邏輯日志;
binlog二進制日志是server層的無論MySQL用什么引擎都會有的,主要是作主從復制,時間點恢復使用;
redo log重做日志是InnoDB存儲引擎層的,用來保證事務安全;
undo log回滾日志保存了事務發生之前的數據的一個版本,可以用于回滾,同時可以提供多版本并發控制下的讀(MVCC),也即非鎖定讀。
crash-safe
InnoDB 就可以保證即使數據庫發生異常重啟,之前提交的記錄都不會丟失,這個能力稱為 crash-safe。
舉個列子:當我們修改的時候,寫完內存了(buffer),但數據還沒真正寫到磁盤的時候。此時我們的數據庫掛了,我們可以對數據進行恢復。
二、bin log
binlog用于記錄數據庫執行的寫入性操作(不包括查詢)信息,以二進制的形式保存在磁盤中。binlog是mysql的邏輯日志,并且由Server層進行記錄,使用任何存儲引擎的mysql數據庫都會記錄binlog日志。可以簡單的理解為它存儲著每條變更的SQL語句。
可以通過binlog來對數據進行恢復;
binlog 可以用于主從復制中,從庫利用主庫上的 binlog 進行重播,實現主從同步。用于數據庫的基于時間點、位點等的還原操作。binlog 的模式分三種:Statement、Row、Mixed;
binlog是通過追加的方式進行寫入的,可以通過max_binlog_size參數設置每個binlog文件的大小,當文件大小達到給定值之后,會生成新的文件來保存日志;
邏輯日志:可以簡單理解為記錄的就是sql語句;
物理日志:mysql數據最終是保存在數據頁中的,物理日志記錄的就是數據頁變更。
binlog使用場景
在實際應用中,binlog的主要使用場景有兩個,分別是?主從復制?和?數據恢復。
主從復制?:在Master端開啟binlog,然后將binlog發送到各個Slave端,Slave端重放binlog從而達到主從數據一致。
數據恢復?:通過使用mysqlbinlog工具來恢復數據。
binlog刷盤時機
對于InnoDB存儲引擎而言,只有在事務提交時才會記錄biglog,此時記錄還在內存中,那么biglog是什么時候刷到磁盤中的呢?mysql通過sync_binlog參數控制biglog的刷盤時機,取值范圍是0-N:
0:不去強制要求,由系統自行判斷何時寫入磁盤;
1:每次commit的時候都要將binlog寫入磁盤;
N:每N個事務,才會將binlog寫入磁盤。
從上面可以看出,sync_binlog最安全的是設置是1,這也是MySQL 5.7.7之后版本的默認值。但是設置一個大一些的值可以提升數據庫性能,因此實際情況下也可以將值適當調大,犧牲一定的一致性來獲取更好的性能。
bin-log三種模式
Statement 模式
每一條修改數據的 sql 都會記錄到 master 的 binlog 中,slave 在復制的時候,sql 進程會解析成和原來在 master 端執行時的相同的 sql 再執行。
優點:在 statement 模式下首先就是解決了 row 模式的缺點,不需要記錄每一行數據的變化,從而減少了 binlog 的日志量,節省了 I/O 以及存儲資源,提高性能。因為它只需要記錄在 master 上執行的語句的細節以及執行語句的上下文信息。
缺點:在 statement 模式下,由于它是記錄的執行語句,所以,為了讓這些語句在 slave 端也能正確執行,那么它還必須記錄每條語句在執行的時候的一些相關信息,即上下文信息,以保證所有語句在 slave 端和在 master 端執行結果相同。另外就是,由于 MySQL 現在發展比較快,很多新功能不斷的加入,使 MySQL 的復制遇到了不小的挑戰,自然復制的時候涉及到越復雜的內容,bug 也就越容易出現。在statement 中,目前已經發現不少情況會造成 MySQL 的復制出現問題,主要是在修改數據的時候使用了某些特定的函數或者功能才會出現,比如:sleep() 函數在有些版本中就不能被正確復制,在存儲過程中使用了 last_insert_id() 函數,可能會使 slave 和 master 上得到不一致的 id 等等。由于 row 模式是基于每一行來記錄變化的,所以不會出現類似的問題。
Row 模式
日志中會記錄每一行數據被修改的形式,然后在 slave 端再對相同的數據進行修改。row 模式只記錄要修改的數據,只有 value,不會有 sql 多表關聯的情況。
優點:在 row 模式下,binlog 中可以不記錄執行的 sql 語句的上下文相關的信息,僅僅只需要記錄哪一條記錄被修改了,修改成什么樣了,所以 row 的日志內容會非常清楚的記錄下每一行數據的修改細節,非常容易理解。而且不會出現某些特定情況下的存儲過程和 function,以及 trigger 的調用和觸發無法被正確復制問題。
缺點:在 row 模式下,當所有執行語句記錄到日志中的時候,都將以每行記錄的修改來記錄,這樣可能會產生大量的日志內容。
Mixed 模式
基于STATMENT和ROW兩種模式的混合復制(mixed-based replication, MBR),一般的復制使用STATEMENT模式保存binlog,對于STATEMENT模式無法復制的操作使用ROW模式保存binlog。比如遇到表結構變更的時候就會以 statement 模式來記錄,如果 SQL 語句確實就是 update 或者 delete 等修改數據的語句,那么還是會記錄所有行的變更即采用ROW模式。
bin log生命周期
事務提交的時候,一次性將事務中的 sql 語句(一個事務可能對應多個 sql 語句)按照一定的格式記錄到 binlog 中,這里與 redo log 很明顯的差異就是 redo log 并不一定是在事務提交的時候才刷新到磁盤,而是在事務開始之后就開始逐步寫入磁盤。binlog 的默認保存時間是由參數 expire_logs_days 配置的,對于非活動的日志文件,在生成時間超過 expire_logs_days 配置的天數之后,會被自動刪除。
三、redo log
redo log 是物理日志,記載著每次在某個頁上做了什么修改。寫redo log也是需要寫磁盤的,但它的好處就是順序IO(我們都知道順序IO比隨機IO快非常多)。寫入的速度很快。
為什么需要redo log?
我們都知道,事務的四大特性里面有一個是?持久性,具體來說就是只要事務提交成功,那么對數據庫做的修改就被永久保存下來了,不可能因為任何原因再回到原來的狀態。
那么mysql是如何保證一致性的呢?最簡單的做法是在每次事務提交的時候,將該事務涉及修改的數據頁全部刷新到磁盤中。但是這么做會有嚴重的性能問題,主要體現在兩個方面:
因為Innodb是以頁為單位進行磁盤交互的,而一個事務很可能只修改一個數據頁里面的幾個字節,這個時候將完整的數據頁刷到磁盤的話,太浪費資源了!
一個事務可能涉及修改多個數據頁,并且這些數據頁在物理上并不連續,使用隨機IO寫入性能太差!
因此mysql設計了redo log,具體來說就是只記錄事務對數據頁做了哪些修改,這樣就能完美地解決性能問題了(相對而言文件更小并且是順序IO)。
redo log基本概念
redo log包括兩部分:一個是內存中的日志緩沖(redo log buffer),另一個是磁盤上的日志文件(redo logfile)。mysql每執行一條DML語句,先將記錄寫入redo log buffer,后續某個時間點再一次性將多個操作記錄寫到redo log file。這種?先寫日志,再寫磁盤?的技術就是MySQL里經常說到的WAL(Write-Ahead Logging)技術。在計算機操作系統中,用戶空間(user space)下的緩沖區數據一般情況下是無法直接寫入磁盤的,中間必須經過操作系統內核空間(kernel space)緩沖區(OS Buffer)。因此,redo log buffer寫入redo logfile實際上是先寫入OS Buffer,然后再通過系統調用fsync()將其刷到redo log file中。
mysql支持三種將redo log buffer寫入redo log file的時機,可以通過innodb_flush_log_at_trx_commit參數配置,各參數值含義如下:
0:延遲寫。不會在事務提交時立即將redo log buffer寫入到os buffer,而是每秒寫入os buffer,然后立即寫入到redo log file,也就是每秒刷盤;
1:實時寫,實時刷。每次事務提交都會將redo log buffer寫入os buffer,然后立即寫入redo log file。數據能夠及時入盤,但是每次事務提交都會刷盤,效率較低;
2:實時寫,延時刷。每次事務提交都將redo log buffer寫入os buffer,然后每秒將os buffer寫入redo log file。
redo log記錄形式
前面說過,redo log實際上記錄數據頁的變更,而這種變更記錄是沒必要全部保存,因此redo log實現上采用了大小固定,循環寫入的方式,當寫到結尾時,會回到開頭循環寫日志。
在innodb中,既有redo log需要刷盤,還有數據頁也需要刷盤,redo log存在的意義主要就是降低對數據頁刷盤的要求 ** 。在上圖中,write pos表示redo log當前記錄的LSN(邏輯序列號)位置,check point表示?數據頁更改記錄?刷盤后對應redo log所處的LSN(邏輯序列號)位置。write pos到check point之間的部分是redo log空著的部分,用于記錄新的記錄;check point到write pos之間是redo log待落盤的數據頁更改記錄。當write pos追上check point時,會先推動check point向前移動,空出位置再記錄新的日志。啟動innodb的時候,不管上次是正常關閉還是異常關閉,總是會進行恢復操作。因為redo log記錄的是數據頁的物理變化,因此恢復的時候速度比邏輯日志(如binlog)要快很多。重啟innodb時,首先會檢查磁盤中數據頁的LSN,如果數據頁的LSN小于日志中的LSN,則會從checkpoint開始恢復。還有一種情況,在宕機前正處于checkpoint的刷盤過程,且數據頁的刷盤進度超過了日志頁的刷盤進度,此時會出現數據頁中記錄的LSN大于日志中的LSN,這時超出日志進度的部分將不會重做,因為這本身就表示已經做過的事情,無需再重做。
redo log與binlog區別
由binlog和redo log的區別可知:binlog日志只用于歸檔,只依靠binlog是沒有crash-safe能力的。但只有redo log也不行,因為redo log是InnoDB特有的,且日志上的記錄落盤后會被覆蓋掉。因此需要binlog和redo log二者同時記錄,才能保證當數據庫發生宕機重啟時,數據不會丟失。
四、undo log
數據庫事務四大特性中有一個是?原子性?,具體來說就是?原子性是指對數據庫的一系列操作,要么全部成功,要么全部失敗,不可能出現部分成功的情況。實際上,?原子性?底層就是通過undo log實現的。undo log主要記錄了數據的邏輯變化,比如一條INSERT語句,對應一條DELETE的undo log,對于每個UPDATE語句,對應一條相反的UPDATE的undo log,這樣在發生錯誤時,就能回滾到事務之前的數據狀態。同時,undo log也是MVCC(多版本并發控制)實現的關鍵。
主要作用
保存了事務發生之前的數據的一個版本,可以用于回滾;
同時可以提供多版本并發控制下的讀(MVCC),也即非鎖定讀。
生命周期
事務開始之前,將當前事務版本生成 undo log,undo log 也會產生 redo log 來保證 undo log 的可靠性。
當事務提交之后,undo log 并不能立馬被刪除,而是放入待清理的鏈表。
由purge 線程判斷是否有其它事務在使用 undo 段中表的上一個事務之前的版本信息,從而決定是否可以清理 undo log 的日志空間。
存儲內容
undo log 存儲的是邏輯格式的日志,保存了事務發生之前的上一個版本的數據,可以用于回滾。當一個舊的事務需要讀取數據時,為了能讀取到老版本的數據,需要順著 undo 鏈找到滿足其可見性的記錄。
存儲位置
默認情況下,undo 文件是保存在共享表空間的,也即 ibdatafile 文件中,當數據庫中發生一些大的事務性操作的時候,要生成大量的 undo log 信息,這些信息全部保存在共享表空間中,因此共享表空間可能會變得很大,默認情況下,也就是 undo log 使用共享表空間的時候,被“撐大”的共享表空間是不會、也不能自動收縮的。因此,MySQL5.7 之后的“獨立 undo 表空間”的配置就顯得很有必要了。
五、兩階段提交
MySQL通過兩階段提交來保證redo log和binlog的數據是一致的。
MySQL最開始是沒有InnoDB引擎的,binlog日志位于Server層,只是用于歸檔和主從復制,本身不具備crash safe的能力。而InnoDB依靠redo log具備了crash safe的能力,redo log和bin log同時記錄,就需要保證兩者的一致性。兩個log的寫入流程是:
寫入redo log->事務狀態設置為prepare->寫入bin log->提交事務->修改redo log事務狀態為commit
將 redo log 的寫入拆成了兩個步驟:prepare 和 commit,先prepare后commit,這個稱為兩段提交。
那么為什么需要兩個段提交呢?redo log和binlog是兩種不同的日志,就類似于分布式中的多節點提交請求,需要保證事務的一致性。redo log和binlog有一個公共字段XID,代表事務ID。當參數innodb_support_xa打開時,在執行事務的第一條SQL時候會去注冊XA,根據第一條SQL的query id拼湊XID數據,然后存儲在事務對象中。
如果兩個日志單純的分開提交,則可能會引發一些問題,如果簡單分開提交,那么對于一條更新語句執行,有兩種情況:
先寫binlog,后寫redo log:如果binlog寫入了,在寫redo log之前數據庫宕機。那么在重啟恢復的時候,通過binlog恢復了數據沒問題。但是由于redo log沒有寫入,這個事務應該無效,也就是原庫中就不應該有這條語句對應的更新。但是通過binlog恢復數據后,數據庫中就多了這條更新
先寫redo log,后寫binlog:如果redo log寫入了,在寫binlog之前數據庫宕機。那么在重啟恢復的時候,通過binlog恢復從庫,那么相對于主庫來說,從庫就少了這條更新
采取了兩段提交之后,怎么做crash恢復呢?如果在寫入binlog之前宕機了,那么事務需要回滾;如果事務commit之前宕機了,那么此時binlog cache中的數據可能還沒有刷盤,那么驗證binlog的完整性:到redo log中找到最近事務的XID,根據這個XID到binlog中去找(XID Event),如果找到了,說明在binlog中對應事務已經提交,那么提交redo log中事務即可;否則需要回滾事務。
栗子
updateTsetc=c+1whereID=2;
下圖是這個 update 語句的執行流程圖,圖中綠色框表示是在 InnoDB 內部執行的,灰色框表示是在執行器中執行的。