在事務的實現機制上,MySQL 采用的是 WAL(Write-ahead logging,預寫式日志)機制來實現的。
在使用 WAL 的系統中,所有的修改都先被寫入到日志中,然后再被應用到系統中。通常包含 redo 和 undo 兩部分信息。
為什么需要使用 WAL,然后包含 redo 和 undo 信息呢?舉個例子,如果一個系統直接將變更應用到系統狀態中,那么在機器掉電重啟之后系統需要知道操作是成功了,還是只有部分成功或者是失敗了(為了恢復狀態)。如果使用了WAL,那么在重啟之后系統可以通過比較日志和系統狀態來決定是繼續完成操作還是撤銷操作。
redo log 稱為重做日志,每當有操作時,在數據變更之前將操作寫入 redo log,這樣當發生掉電之類的情況時系統可以在重啟后繼續操作。
undo log 稱為撤銷日志,當一些變更執行到一半無法完成時,可以根據撤銷日志恢復到變更之間的狀態。
MySQL 中用 redo log 來在系統 Crash 重啟之類的情況時修復數據(事務的持久性),而 undo log 來保證事務的原子性。
7.1. redo 日志
7.1.1. redo 日志的作用
InnoDB 存儲引擎是以頁為單位來管理存儲空間的,我們進行的增刪改查操作其實本質上都是在訪問頁面(包括讀頁面、寫頁面、創建新頁面等操作)。在Buffer Pool 的時候說過,在真正訪問頁面之前,需要把在磁盤上的頁緩存到內存中的 Buffer Pool 之后才可以訪問。但是在事務的時候又強調過一個稱之為持久性的特性,就是說對于一個已經提交的事務,在事務提交后即使系統發生了崩潰,這個事務對數據庫中所做的更改也不能丟失。
如果我們只在內存的 Buffer Pool 中修改了頁面,假設在事務提交后突然發生了某個故障,導致內存中的數據都失效了,那么這個已經提交了的事務對數據庫中所做的更改也就跟著丟失了,這是我們所不能忍受的。那么如何保證這個持久性呢?一個很簡單的做法就是在事務提交完成之前把該事務所修改的所有頁面都刷新到磁盤,但是這個簡單粗暴的做法有些問題:
刷新一個完整的數據頁太浪費了
有時候我們僅僅修改了某個頁面中的一個字節,但是我們知道在 InnoDB 中是以頁為單位來進行磁盤 IO 的,也就是說我們在該事務提交時不得不將一個完整的頁面從內存中刷新到磁盤,我們又知道一個頁面默認是 16KB 大小,只修改一個字節就要刷新 16KB 的數據到磁盤上顯然是太浪費了。
隨機 IO 刷起來比較慢
一個事務可能包含很多語句,即使是一條語句也可能修改許多頁面,該事務修改的這些頁面可能并不相鄰,這就意味著在將某個事務修改的 Buffer Pool 中的頁面刷新到磁盤時,需要進行很多的隨機 IO,隨機 IO 比順序 IO 要慢,尤其對于傳統的機械硬盤來說。
怎么辦呢?我們只是想讓已經提交了的事務對數據庫中數據所做的修改永
久生效,即使后來系統崩潰,在重啟后也能把這種修改恢復出來。所以我們其實沒有必要在每次事務提交時就把該事務在內存中修改過的全部頁面刷新到磁盤,只需要把修改了哪些東西記錄一下就好,比方說某個事務將系統表空間中的第100 號頁面中偏移量為 1000 處的那個字節的值 1 改成 2 我們只需要記錄一下:
將第 0 號表空間的 100 號頁面的偏移量為 1000 處的值更新為 2。
這樣我們在事務提交時,把上述內容刷新到磁盤中,即使之后系統崩潰了,重啟之后只要按照上述內容所記錄的步驟重新更新一下數據頁,那么該事務對數據庫中所做的修改又可以被恢復出來,也就意味著滿足持久性的要求。因為在系統崩潰重啟時需要按照上述內容所記錄的步驟重新更新數據頁,所以上述內容也被稱之為重做日志,英文名為 redo log,也可以稱之為 redo 日志。與在事務提交時將所有修改過的內存中的頁面刷新到磁盤中相比,只將該事務執行過程中產生的 redo 日志刷新到磁盤的好處如下:
1、redo 日志占用的空間非常小
存儲表空間 ID、頁號、偏移量以及需要更新的值所需的存儲空間是很小的。
2、redo 日志是順序寫入磁盤的
在執行事務的過程中,每執行一條語句,就可能產生若干條 redo 日志,這些日志是按照產生的順序寫入磁盤的,也就是使用順序 IO。
7.1.2. redo 日志格式
通過上邊的內容我們知道,redo 日志本質上只是記錄了一下事務對數據庫做了哪些修改。 InnoDB 們針對事務對數據庫的不同修改場景定義了多種類型的redo 日志,但是絕大部分類型的 redo 日志都有下邊這種通用的結構:
各個部分的詳細釋義如下:
type:該條 redo 日志的類型,redo 日志設計大約有 53 種不同的類型日志。
- space ID:表空間 ID。
- page number:頁號。
- data:該條 redo 日志的具體內容。
7.1.2.1. 簡單的 redo 日志類
我們用一個簡單的例子來說明最基本的 redo 日志類型。我們前邊介紹
InnoDB 的記錄行格式的時候說過,如果我們沒有為某個表顯式的定義主鍵,并且表中也沒有定義 Unique 鍵,那么 InnoDB 會自動的為表添加一個稱之為 row_id的隱藏列作為主鍵。為這個 row_id 隱藏列賦值的方式如下:
服務器會在內存中維護一個全局變量,每當向某個包含隱藏的 row_id 列的表中插入一條記錄時,就會把該變量的值當作新記錄的 row_id 列的值,并且把該變量自增 1。
每當這個變量的值為 256 的倍數時,就會將該變量的值刷新到系統表空間的頁號為 7 的頁面中一個稱之為 Max Row ID 的屬性處。
當系統啟動時,會將上邊提到的 Max Row ID 屬性加載到內存中,將該值加上 256 之后賦值給我們前邊提到的全局變量。
這個 Max Row ID 屬性占用的存儲空間是 8 個字節,當某個事務向某個包含row_id 隱藏列的表插入一條記錄,并且為該記錄分配的 row_id 值為 256 的倍數時,就會向系統表空間頁號為 7 的頁面的相應偏移量處寫入 8 個字節的值。但是我們要知道,這個寫入實際上是在 Buffer Pool 中完成的,我們需要為這個頁面的修改記錄一條 redo 日志,以便在系統崩潰后能將已經提交的該事務對該頁面所做的修改恢復出來。這種情況下對頁面的修改是極其簡單的,redo 日志中只需要記錄一下在某個頁面的某個偏移量處修改了幾個字節的值,具體被修改的內容是啥就好了,InnoDB 把這種極其簡單的 redo 日志稱之為物理日志,并且根據在頁面中寫入數據的多少劃分了幾種不同的 redo 日志類型:
- MLOG_1BYTE(type 字段對應的十進制數字為 1):表示在頁面的某個偏移量處寫入 1 個字節的 redo 日志類型。
- MLOG_2BYTE(type 字段對應的十進制數字為 2):表示在頁面的某個偏移量處寫入 2 個字節的 redo 日志類型。
- MLOG_4BYTE(type 字段對應的十進制數字為 4):表示在頁面的某個偏移量處寫入 4 個字節的 redo 日志類型。
- MLOG_8BYTE(type 字段對應的十進制數字為 8):表示在頁面的某個偏移量處寫入 8 個字節的 redo 日志類型。
- MLOG_WRITE_STRING(type 字段對應的十進制數字為 30):表示在頁面的某個偏移量處寫入一串數據。
我們上邊提到的 Max Row ID 屬性實際占用 8 個字節的存儲空間,所以在修改頁面中的該屬性時,會記錄一條類型為MLOG_8BYTE的redo日志,MLOG_8BYTE的 redo 日志結構如下所示:
offset 代表在頁面中的偏移量。
其余 MLOG_1BYTE、MLOG_2BYTE、MLOG_4BYTE 類型的 redo 日志結構和MLOG_8BYTE 的類似,只不過具體數據中包含對應個字節的數據罷了。
MLOG_WRITE_STRING 類型的 redo 日志表示寫入一串數據,但是因為不能確定寫入的具體數據占用多少字節,所以需要在日志結構中還會多一個 len 字段。
7.1.2.2. 復雜一些的 redo 日志類型
有時候執行一條語句會修改非常多的頁面,包括系統數據頁面和用戶數據頁面(用戶數據指的就是聚簇索引和二級索引對應的 B+樹)。以一條 INSERT 語句為例,它除了要向 B+樹的頁面中插入數據,也可能更新系統數據 Max Row ID 的值,不過對于我們用戶來說,平時更關心的是語句對 B+樹所做更新:
表中包含多少個索引,一條 INSERT 語句就可能更新多少棵 B+樹。
針對某一棵 B+樹來說,既可能更新葉子節點頁面,也可能更新非葉子節點頁面,也可能創建新的頁面(在該記錄插入的葉子節點的剩余空間比較少,不足以存放該記錄時,會進行頁面的分裂,在非葉子節點頁面中添加目錄項記錄)。
在語句執行過程中,INSERT 語句對所有頁面的修改都得保存到 redo 日志中去。實現起來是非常麻煩的,比方說將記錄插入到聚簇索引中時,如果定位到的葉子節點的剩余空間足夠存儲該記錄時,那么只更新該葉子節點頁面就好,那么只記錄一條 MLOG_WRITE_STRING 類型的 redo 日志,表明在頁面的某個偏移量處增加了哪些數據就好了么?
別忘了一個數據頁中除了存儲實際的記錄之后,還有什么 File Header、PageHeader、Page Directory 等等部分,所以每往葉子節點代表的數據頁里插入一條記錄時,還有其他很多地方會跟著更新,比如說:
可能更新 Page Directory 中的槽信息、Page Header 中的各種頁面統計信息,比如槽數量可能會更改,還未使用的空間最小地址可能會更改,本頁面中的記錄數量可能會更改,各種信息都可能會被修改,同時數據頁里的記錄是按照索引列從小到大的順序組成一個單向鏈表的,每插入一條記錄,還需要更新上一條記錄的記錄頭信息中的 next_record 屬性來維護這個單向鏈表。
畫一個簡易的示意圖就像是這樣:
其實說到底,把一條記錄插入到一個頁面時需要更改的地方非常多。這時我們如果使用上邊介紹的簡單的物理 redo 日志來記錄這些修改時,可以有兩種解決方案:
- 方案一:在每個修改的地方都記錄一條 redo 日志。
也就是如上圖所示,有多少個加粗的塊,就寫多少條物理 redo 日志。這樣子記錄 redo 日志的缺點是顯而易見的,因為被修改的地方是在太多了,可能記錄的 redo 日志占用的空間都比整個頁面占用的空間都多了。 - 方案二:將整個頁面的第一個被修改的字節到最后一個修改的字節之間所有的數據當成是一條物理 redo 日志中的具體數據。
從圖中也可以看出來,第一個被修改的字節到最后一個修改的字節之間仍然有許多沒有修改過的數據,我們把這些沒有修改的數據也加入到 redo 日志中去依然很浪費。
正因為上述兩種使用物理 redo 日志的方式來記錄某個頁面中做了哪些修改比較浪費,InnoDB 中就有非常多的 redo 日志類型來做記錄。
這些類型的 redo 日志既包含物理層面的意思,也包含邏輯層面的意思,具體指:
物理層面看,這些日志都指明了對哪個表空間的哪個頁進行了修改。
邏輯層面看,在系統崩潰重啟時,并不能直接根據這些日志里的記載,將頁面內的某個偏移量處恢復成某個數據,而是需要調用一些事先準備好的函數,執行完這些函數后才可以將頁面恢復成系統崩潰前的樣子。
簡單來說,一個 redo 日志類型而只是把在本頁面中變動(比如插入、修改)一條記錄所有必備的要素記了下來,之后系統崩潰重啟時,服務器會調用相關向某個頁面變動(比如插入、修改)一條記錄的那個函數,而 redo 日志中的那些數據就可以被當成是調用這個函數所需的參數,在調用完該函數后,頁面中的相關值也就都被恢復到系統崩潰前的樣子了。這就是所謂的邏輯日志的意思。
當然,如果不是為了寫一個解析 redo 日志的工具或者自己開發一套 redo 日志系統的話,那就不需要去研究 InnoDB 中的 redo 日志具體格式。
大家只要記住:redo 日志會把事務在執行過程中對數據庫所做的所有修改都記錄下來,在之后系統崩潰重啟后可以把事務所做的任何修改都恢復出來。
7.1.3. Mini-Transaction
7.1.3.1. 以組的形式寫入 redo 日志
語句在執行過程中可能修改若干個頁面。比如我們前邊說的一條 INSERT 語句可能修改系統表空間頁號為 7 的頁面的 Max Row ID 屬性(當然也可能更新別的系統頁面,只不過我們沒有都列舉出來而已),還會更新聚簇索引和二級索引對應 B+樹中的頁面。由于對這些頁面的更改都發生在 Buffer Pool 中,所以在修改完頁面之后,需要記錄一下相應的 redo 日志。
在這個執行語句的過程中產生的 redo 日志被 InnoDB 人為的劃分成了若干個不可分割的組,比如:
1、更新 Max Row ID 屬性時產生的 redo 日志是不可分割的。
2、向聚簇索引對應 B+樹的頁面中插入一條記錄時產生的 redo 日志是不可分割的。
3、向某個二級索引對應 B+樹的頁面中插入一條記錄時產生的 redo 日志是不可分割的。
4、還有其他的一些對頁面的訪問操作時產生的 redo 日志是不可分割的….。
怎么理解這個不可分割的意思呢?我們以向某個索引對應的 B+樹插入一條記錄為例,在向 B+樹中插入這條記錄之前,需要先定位到這條記錄應該被插入到哪個葉子節點代表的數據頁中,定位到具體的數據頁之后,有兩種可能的情況:
情況二:該數據頁剩余的空閑空間不足,那么事情就很麻煩了,遇到這種情況要進行所謂的頁分裂操作:
1、新建一個葉子節點;
2、然后把原先數據頁中的一部分記錄復制到這個新的數據頁中;
3、然后再把記錄插入進去,把這個葉子節點插入到葉子節點鏈表中;
4、非葉子節點中添加一條目錄項記錄指向這個新創建的頁面;
5、非葉子節點空間不足,繼續分裂。
很顯然,這個過程要對多個頁面進行修改,也就意味著會產生很多條 redo日志,我們把這種情況稱之為悲觀插入。
另外,這個過程中,由于需要新申請數據頁,還需要改動一些系統頁面,比方說要修改各種段、區的統計信息信息,各種鏈表的統計信息,也會產生 redo日志。
當然在樂觀插入時也可能產生多條 redo 日志。
InnoDB 認為向某個索引對應的 B+樹中插入一條記錄的這個過程必須是原子的,不能說插了一半之后就停止了。比方說在悲觀插入過程中,新的頁面已經分配好了,數據也復制過去了,新的記錄也插入到頁面中了,可是沒有向非葉子節點中插入一條目錄項記錄,這個插入過程就是不完整的,這樣會形成一棵不正確的 B+樹。
我們知道 redo 日志是為了在系統崩潰重啟時恢復崩潰前的狀態,如果在悲觀插入的過程中只記錄了一部分 redo 日志,那么在系統崩潰重啟時會將索引對應的 B+樹恢復成一種不正確的狀態。
所以規定在執行這些需要保證原子性的操作時必須以組的形式來記錄的redo 日志,在進行系統崩潰重啟恢復時,針對某個組中的 redo 日志,要么把全部的日志都恢復掉,要么一條也不恢復。在實現上,根據多個 redo 日志的不同,使用了特殊的 redo 日志類型作為組的結尾,來表示一組完整的 redo 日志。
7.1.3.2. Mini-Transaction 的概念
所以 MySQL 把對底層頁面中的一次原子訪問的過程稱之為一個Mini-Transaction,比如上邊所說的修改一次 Max Row ID 的值算是一個
Mini-Transaction,向某個索引對應的 B+樹中插入一條記錄的過程也算是一個Mini-Transaction。
一個所謂的 Mini-Transaction 可以包含一組 redo 日志,在進行崩潰恢復時這一組 redo 日志作為一個不可分割的整體。
一個事務可以包含若干條語句,每一條語句其實是由若干個 Mini-Transaction組成,每一個 Mini-Transaction 又可以包含若干條 redo 日志,最終形成了一個樹形結構。
7.1.4. redo 日志的寫入過程
7.1.4.1. redo log block 和日志緩沖區
InnoDB 為了更好的進行系統崩潰恢復,把通過 Mini-Transaction 生成的 redo日志都放在了大小為 512 字節的塊(block)中。。
我們前邊說過,為了解決磁盤速度過慢的問題而引入了 Buffer Pool。同理,寫入 redo 日志時也不能直接直接寫到磁盤上,實際上在服務器啟動時就向操作系統申請了一大片稱之為 redo log buffer 的連續內存空間,翻譯成中文就是 redo日志緩沖區,我們也可以簡稱為 log buffer。這片內存空間被劃分成若干個連續的 redo log block,我們可以通過啟動參數 innodb_log_buffer_size 來指定 log buffer的大小,該啟動參數的默認值為 16MB。
向 log buffer 中寫入 redo 日志的過程是順序的,也就是先往前邊的 block 中寫,當該 block 的空閑空間用完之后再往下一個 block 中寫。
我們前邊說過一個 Mini-Transaction 執行過程中可能產生若干條 redo 日志,這些 redo 日志是一個不可分割的組,所以其實并不是每生成一條 redo 日志,就將其插入到 log buffer 中,而是每個 Mini-Transaction 運行過程中產生的日志先暫時存到一個地方,當該 Mini-Transaction 結束的時候,將過程中產生的一組 redo日志再全部復制到 log buffer 中。
7.1.4.2. redo 日志刷盤時機
我們前邊說 Mini-Transaction 運行過程中產生的一組 redo 日志在
Mini-Transaction 結束時會被復制到 log buffer 中,可是這些日志總在內存里呆著也不是個辦法,在一些情況下它們會被刷新到磁盤里,比如:
1、log buffer 空間不足時,log buffer 的大小是有限的(通過系統變量
innodb_log_buffer_size 指定),如果不停的往這個有限大小的 log buffer 里塞入日志,很快它就會被填滿。InnoDB 認為如果當前寫入 log buffer 的 redo 日志量已經占滿了 log buffer 總容量的大約一半左右,就需要把這些日志刷新到磁盤上。
2、事務提交時,我們前邊說過之所以使用 redo 日志主要是因為它占用的空間少,還是順序寫,在事務提交時可以不把修改過的 Buffer Pool 頁面刷新到磁盤,但是為了保證持久性,必須要把修改這些頁面對應的 redo 日志刷新到磁盤。
3、后臺有一個線程,大約每秒都會刷新一次 log buffer 中的 redo 日志到磁盤。
4、正常關閉服務器時等等。
7.1.4.3. redo 日志文件組
MySQL 的數據目錄(使用 SHOW VARIABLES LIKE 'datadir’查看)下默認有兩個名為 ib_logfile0 和 ib_logfile1 的文件,log buffer 中的日志默認情況下就是刷新到這兩個磁盤文件中。如果我們對默認的 redo 日志文件不滿意,可以通過下邊幾個啟動參數來調節:
innodb_log_group_home_dir,該參數指定了 redo 日志文件所在的目錄,默認值就是當前的數據目錄。
- innodb_log_file_size,該參數指定了每個 redo 日志文件的大小,默認值為 48MB,
- innodb_log_files_in_group,該參數指定 redo 日志文件的個數,默認值為 2,最大值為 100。
所以磁盤上的 redo 日志文件可以不只一個,而是以一個日志文件組的形式出現的。這些文件以 ib_logfile[數字](數字可以是 0、1、2…)的形式進行命名。
在將 redo 日志寫入日志文件組時,是從 ib_logfile0 開始寫,如果 ib_logfile0 寫滿了,就接著 ib_logfile1 寫,同理,ib_logfile1 寫滿了就去寫 ib_logfile2,依此類推。
如果寫到最后一個文件該咋辦?那就重新轉到 ib_logfile0 繼續寫。
7.1.4.4. redo 日志文件格式
我們前邊說過 log buffer 本質上是一片連續的內存空間,被劃分成了若干個512 字節大小的 block。將 log buffer 中的 redo 日志刷新到磁盤的本質就是把 block的鏡像寫入日志文件中,所以 redo 日志文件其實也是由若干個 512 字節大小的block 組成。
redo 日志文件組中的每個文件大小都一樣,格式也一樣,都是由兩部分組成:前 2048 個字節,也就是前 4 個 block 是用來存儲一些管理信息的。
從第 2048 字節往后是用來存儲 log buffer 中的 block 鏡像的。
7.1.5. Log Sequence Number
自系統開始運行,就不斷的在修改頁面,也就意味著會不斷的生成 redo 日志。redo 日志的量在不斷的遞增,就像人的年齡一樣,自打出生起就不斷遞增,永遠不可能縮減了。
InnoDB 為記錄已經寫入的 redo 日志量,設計了一個稱之為 Log Sequence Number 的全局變量,翻譯過來就是:日志序列號,簡稱 LSN。規定初始的 lsn 值為 8704(也就是一條 redo 日志也沒寫入時,LSN 的值為 8704)。
我們知道在向 log buffer 中寫入 redo 日志時不是一條一條寫入的,而是以一個 Mini-Transaction 生成的一組 redo 日志為單位進行寫入的。從上邊的描述中可以看出來,每一組由 Mini-Transaction 生成的 redo 日志都有一個唯一的 LSN 值與其對應,LSN 值越小,說明 redo 日志產生的越早。
7.1.5.1. flushed_to_disk_lsn
redo 日志是首先寫到 log buffer 中,之后才會被刷新到磁盤上的 redo 日志文件。InnoDB 中有一個稱之為 buf_next_to_write 的全局變量,標記當前 log buffer中已經有哪些日志被刷新到磁盤中了。
我們前邊說 lsn 是表示當前系統中寫入的 redo 日志量,這包括了寫到 logbuffer 而沒有刷新到磁盤的日志,相應的,InnoDB 也有一個表示刷新到磁盤中的redo 日志量的全局變量,稱之為flushed_to_disk_lsn。系統第一次啟動時,該變量的值和初始的 lsn 值是相同的,都是 8704。隨著系統的運行,redo 日志被不斷寫入 log buffer,但是并不會立即刷新到磁盤,lsn 的值就和 flushed_to_disk_lsn的值拉開了差距。我們演示一下:
系統第一次啟動后,向 log buffer 中寫入了 mtr_1、mtr_2、mtr_3 這三個 mtr產生的 redo 日志,假設這三個 mtr 開始和結束時對應的 lsn 值分別是:
- mtr_1:8716 ~ 8916
- mtr_2:8916 ~ 9948
- mtr_3:9948 ~ 10000
此時的 lsn 已經增長到了 10000,但是由于沒有刷新操作,所以此時
flushed_to_disk_lsn 的值仍為 8704。
隨后進行將 log buffer 中的 block 刷新到 redo 日志文件的操作,假設將 mtr_1和 mtr_2 的日志刷新到磁盤,那么 flushed_to_disk_lsn 就應該增長 mtr_1 和 mtr_2寫入的日志量,所以 flushed_to_disk_lsn 的值增長到了 9948。
綜上所述,當有新的 redo 日志寫入到 log buffer 時,首先 lsn 的值會增長,但flushed_to_disk_lsn不變,隨后隨著不斷有log buffer中的日志被刷新到磁盤上,flushed_to_disk_lsn 的值也跟著增長。如果兩者的值相同時,說明 log buffer 中的所有 redo 日志都已經刷新到磁盤中了。
Tips:應用程序向磁盤寫入文件時其實是先寫到操作系統的緩沖區中去,如果某個寫入操作要等到操作系統確認已經寫到磁盤時才返回,那需要調用一下操作系統提供的 fsync 函數。其實只有當系統執行了 fsync 函數后,flushed_to_disk_lsn 的值才會跟著增長,當僅僅把 log buffer 中的日志寫入到操作系統緩沖區卻沒有顯式的刷新到磁盤時,另外的一個稱之為 write_lsn 的值跟著增長。
當然系統的 LSN 值遠不止我們前面描述的 lsn,還有很多。
7.1.5.2. 查看系統中的各種 LSN 值
我們可以使用 SHOW ENGINE INNODB STATUS 命令查看當前 InnoDB 存儲引擎中的各種 LSN 值的情況,比如:
SHOW ENGINE INNODB STATUS\G
其中:
- Log sequence number:代表系統中的 lsn 值,也就是當前系統已經寫入的 redo日志量,包括寫入 log buffer 中的日志。
- Log flushed up to:代表 flushed_to_disk_lsn 的值,也就是當前系統已經寫入磁盤的 redo 日志量。
- Pages flushed up to:代表 flush 鏈表中被最早修改的那個頁面對應的oldest_modification 屬性值。
- Last checkpoint at:當前系統的 checkpoint_lsn 值。
7.1.6. innodb_flush_log_at_trx_commit 的用法
我們前邊說為了保證事務的持久性,用戶線程在事務提交時需要將該事務執行過程中產生的所有 redo 日志都刷新到磁盤上。會很明顯的降低數據庫性能。
如果對事務的持久性要求不是那么強烈的話,可以選擇修改一個稱為
innodb_flush_log_at_trx_commit 的系統變量的值,該變量有 3 個可選的值:
0:當該系統變量值為 0 時,表示在事務提交時不立即向磁盤中同步 redo 日志,這個任務是交給后臺線程做的。
這樣很明顯會加快請求處理速度,但是如果事務提交后服務器掛了,后臺線程沒有及時將 redo 日志刷新到磁盤,那么該事務對頁面的修改會丟失。
1:當該系統變量值為 1 時,表示在事務提交時需要將 redo 日志同步到磁盤,可以保證事務的持久性。1 也是 innodb_flush_log_at_trx_commit 的默認值。
2:當該系統變量值為 2 時,表示在事務提交時需要將 redo 日志寫到操作系統的緩沖區中,但并不需要保證將日志真正的刷新到磁盤。
這種情況下如果數據庫掛了,操作系統沒掛的話,事務的持久性還是可以保證的,但是操作系統也掛了的話,那就不能保證持久性了。
7.1.7. 崩潰后的恢復
7.1.7.1. 恢復機制
在服務器不掛的情況下,redo 日志簡直就是個大累贅,不僅沒用,反而讓性能變得更差。但是萬一數據庫掛了,就可以在重啟時根據 redo 日志中的記錄就可以將頁面恢復到系統崩潰前的狀態。
MySQL 可以根據 redo 日志中的各種 LSN 值,來確定恢復的起點和終點。然后將 redo 日志中的數據,以哈希表的形式,將一個頁面下的放到哈希表的一個槽中。之后就可以遍歷哈希表,因為對同一個頁面進行修改的 redo 日志都放在了一個槽里,所以可以一次性將一個頁面修復好(避免了很多讀取頁面的隨機 IO)。
并且通過各種機制,避免無謂的頁面修復,比如已經刷新的頁面,進而提升崩潰恢復的速度。
7.1.7.2. 崩潰后的恢復為什么不用 binlog?
1、這兩者使用方式不一樣
binlog 會記錄表所有更改操作,包括更新刪除數據,更改表結構等等,主要用于人工恢復數據,而 redo log 對于我們是不可見的,它是 InnoDB 用于保證crash-safe 能力的,也就是在事務提交后 MySQL 崩潰的話,可以保證事務的持久性,即事務提交后其更改是永久性的。
一句話概括:binlog 是用作人工恢復數據,redo log 是 MySQL 自己使用,用于保證在數據庫崩潰時的事務持久性。
2、redo log 是 InnoDB 引擎特有的,binlog 是 MySQL 的 Server 層實現的, 所有引擎都可以使用。
3、redo log 是物理日志,記錄的是“在某個數據頁上做了什么修改”,恢復
的速度更快;binlog 是邏輯日志,記錄的是這個語句的原始邏輯,比如“給 ID=2這的 c 字段加 1 ” ;
4、redo log 是“循環寫”的日志文件,redo log 只會記錄未刷盤的日志,已
經刷入磁盤的數據都會從 redo log 這個有限大小的日志文件里刪除。binlog 是追加日志,保存的是全量的日志。
5、最重要的是,當數據庫 crash 后,想要恢復未刷盤但已經寫入 redo log 和binlog 的數據到內存時,binlog 是無法恢復的。雖然 binlog 擁有全量的日志,但沒有一個標志讓 innoDB 判斷哪些數據已經入表(寫入磁盤),哪些數據還沒有。
比如,binlog 記錄了兩條日志:
給 ID=2 這一行的 c 字段加 1
給 ID=2 這一行的 c 字段加 1
在記錄 1 入表后,記錄 2 未入表時,數據庫 crash。重啟后,只通過 binlog數據庫無法判斷這兩條記錄哪條已經寫入磁盤,哪條沒有寫入磁盤,不管是兩條都恢復至內存,還是都不恢復,對 ID=2 這行數據來說,都不對。
但 redo log 不一樣,只要刷入磁盤的數據,都會從 redo log 中抹掉,數據庫重啟后,直接把 redo log 中的數據都恢復至內存就可以了。