重做日志用來實現事務的持久性,即ACID中的D,由兩部分組成:
一是內存中的重做日志緩沖(redo log buffer) 易丟失
二是重做日志文件(redo log file) 持久的
InnoDB是事務的存儲引擎,其通過Force Log at Commit 機制實現事務的持久性,即當事務提交commit時,必須先將事務的所有日志寫入到重做日志文件進行持久化,待事務COMMIT操作完成才算完成,這里的日志指重做日志,在InnoDB存儲引擎中,由兩部分組成,即redo log 和undo Log. redo log用來保證事務的持久性,undo log 用來幫主事務回滾及MVCC的功能。redo log 基本是順序寫的,在數據庫運行時不需要對redo log的文件進行讀取操作。而undo log 是需要進行隨機讀寫的
為了確保每次日志都能寫入日志文件,在每次將重做日志緩沖寫入重做日志文件后,InnoDB存儲引擎都需要調用一次fsync操作,由於重做日志文件打開並沒有使用O_DIRECT選項,因此重做日志緩沖先寫入文件系統緩存。為了確保重做日志寫入磁盤,必須進行fsync操作。由於fsync的效率取決於磁盤的性能,因此磁盤的性能決定了事務提交的性能,也就是數據庫的性能
InnoDB存儲引擎允許用戶手工設置非持久性的情況發生,以此來提高數據庫的性能。即當事務提交時,日志不寫入重做日志,而是等待一個時間周期后在執行fsync操作。由於並非強制在事務提交時進行一次fsync操作,顯然這可以提高數據庫的性能,但是當數據庫發生宕機時,由於部分日志未書安心到磁盤,因此會丟失最后一段的事務
參數innodb_flush_log_at_trx_commit用來控制重做日志刷新到磁盤的策略,改參數默認為1 ,表示事務提交時必須調用一次fsync操作,還可以設置成0 和2 ,0表示事務提交時不進行寫入重做日志操作,這個操作僅在master thread中完成,而master thread中每一秒會進行一次重做日志文件的fsync操作,2 表示事務提交時將重做日志寫入重做日志文件,但僅寫入文件系統的緩存中,不進行fsync操作。在這個設置下,當MySQL發生宕機而操作系統不發生宕機時,並不會導致事務的丟失,而當操作系統宕機時,重啟數據庫會丟失未從文件系統緩存刷新到重做日志文件的那部分事務
參考一個列子,比較innodb_flush_log_at_trx_commit對事務的影響。首先根據如下代碼創建表t1和存儲過程p_load
CREATE TABLE test_load( a INT, b CHAR(80) )ENGINE=INNODB; DELIMITER // CREATE PROCEDURE p_load(COUNT INT UNSIGNED) BEGIN DECLARE s INT UNSIGNED DEFAULT 1; DECLARE c CHAR(80) DEFAULT REPEAT('a',80); WHILE s<= COUNT DO INSERT INTO test_load SELECT NULL,c; COMMIT; SET s = s+1; END WHILE; END; // DELIMITER ;
存儲過程p_load的作用是將數據不斷的插入表test_load中,並且每插入一條就進行一次顯示的COMMIT操作,在默認的設置下,參數innodb_flush_log_at_trx_commit為1的情況下,InnoDB會將重做日志緩沖寫入文件,並且調用一次fsync操作,如果執行CALL p_load(500 000),則會向表中插入50W行的記錄,執行50W次的fsync操作,先看看默認情況下插入50W條記錄需要的時間
CALL p_load(500000);
虛擬機,插入50W條記錄,開銷18分鍾,對於生產環境來說,時間肯定是不能接受的,而造成時間比較饞的原因是在於fsync所需要的時間,那么將參數設置成
SET GLOBAL innodb_flush_log_at_trx_commit=0;
再執行
CALL p_load(500000); 總耗時40秒
可以看到參數innodb_flush_log_at_trx_commit設置為0后,插入50W行的 記錄縮短了接近17分鍾。造成這么大現象的主要原因是 后者大大減少了fsync的次數,從而挺高了數據庫執行的性能,如下表顯示了innodb_flush_log_at_trx_commit的不同設置下,調用存儲過程p_load插入50W行記錄的時間
雖然用戶可以設置參數innodb_flush_log_at_trx_commit為0或2來提高事務提交的性能,但是需要牢記,這種設置喪失了事務的ACID特性,而針對上述存儲過程,為了提高事務的提交性能,應該在將50W行記錄插入表后進行一次的COMMIT操作,而不是沒插入一條記錄后進行一次COMMIT操作。這樣的好處是還可以使事務方法在回滾時會滾到事務最開始的確定狀態
在MySQL數據庫中還有一種二進制日志其用來進行POINT-IN-TIME(PIT)的恢復及主從復制(Replication)環境的建立,從表面上看和重做日志非常相似,都是記錄了對於數據庫操作的日志,然而,從本質上來看,兩者有非常大的不同
首先,重做日志是InnoDB存儲引擎層產生,而二進制日志是在MySQL數據庫的上層產生,並且二進制日志不僅僅針對InnoDB存儲引擎,MySQL數據庫中任何存儲引擎對於數據庫的編個都會產生二進制日志
其次,兩種日志記錄的內容形式不一樣,MySQL數據庫上層的二進制日志是一種邏輯日志,其記錄的是對應的SQL語句,而InnoDB存儲引擎層面的重做日志是物理格式日志,其記錄的是每個頁的修改
此外,兩種日志記錄寫入磁盤的時間點不同,如圖,二進制日志只在事務提交完成后進行一次寫入,而InnoDB存儲引擎的重做日志在事務進行中不斷地被寫入,這表現為日志並不是隨事務提交的順序進行寫入的
從圖看到,二進制日志近在事務提交時記錄,並且對每一個事務,僅包含對應事務的一個日志,而對於InnoDB存儲引擎的重做日志,由於其記錄的是物理操作日志,因此每個事務對應多個日志條目,並且事務的重做日志是並發的,並非在事務提交時寫入,故其在文件中的記錄順序並非是事務的開始順序。*T1 * T2 *T3表示事務提交時的日志
2 log block
在InnoDB存儲引擎中,重做日志都是以512字節進行存儲的,這意味着重做日志緩存、重做日志文件塊都是以塊block的方式進行保存的,稱為重做日志塊(redo log block)每塊的大小512字節
每個頁中產生的重做日志數量大於512字節,那么需要分割多個重做日志塊進行存儲,此外,由於重做日志快的大小和磁盤扇區大小一樣,都是512字節,因此重做日志的寫入可以保證原子性,不需要double write技術
重做日志快除了日志本身之外,還由日志塊頭(log block header)及日志塊尾(log block tailer)兩部分組成。重做日志頭一共占用12字節,重做日志尾占用8字節。故每個重做日志塊實際可以存儲的大小為492字節(512-12-8),如圖顯示重做日志塊緩存的結構
如圖顯示了重做日志緩存的結果,可以發現,重做日志緩存由每個為512字節大小的日志塊鎖組成,日志塊由三部分組成,依次為日志快頭(log block header)、日志內容(log body)、日志塊尾(log block tailer)
log block header由4部分組成
log buffer 是由log block組成,在內部log buffer就好似一個數組,因此LOG_BLOCK_HDR_NO用來標記這個數組中的位置,尤其是遞增並且循環使用的。占用4個字節。但是由於第一位用來判斷是否是flush bit,所以最大值為2G
LOG_BLOCK_HDR_DATA_LEN占用2個字節,表示log block所占用的大小,當log block被寫滿時,該值為0x200,表示使用全部的log block空間,即占用512字節
LOG_BLOCK_FIRST_REC_GROUP 占用2個字節,表示log block中第一個日志所在的偏移量。如果該值的大小和LOG_BLOCK_HDR_DATA_LEN相同,則表示當前log block不包含新的日志。如事務T1的重做日志1占用762字節,事務T2的重做日志占用100字節,。由於每個log block實際只能保存492字節,因此其在log buffer的情況應該如圖所示
從圖可以觀察到,由於事務T1的重做日志占用792字節,因此需要占用兩個log block。左側的log block中 LOG_BLOCK_FIRST_REC_GROUP為12,級log block中第一個日志的開始位置,在第二個log block中,由於包含了之前事務T1的重做日志,事務T2的日志才是log block中第一日志,因此該log block的LOG_BLOCK_FIRST_REC_GROUP為(270+12)
LOG_BLOCK_CHECKPOINT_NO占用4字節,表示該log block最后被寫入時的檢查點第4字節的值
log block tailer 只由1個部分組成,且值和LOG_BLOCK_HDR_NO相同,並在函數log_block_init中被初始化 LOG_BLOCK_TRL_NO 大小為4字節
3 log group
log group 重做日志組,其中有多個重做日志文件,雖然源碼已經支持log group的景象功能,但是在ha_innobase.cc文件中禁止了該功能,因此,InnoDB存儲引擎實際只由一個log group
log group是一個邏輯的概念,並沒有一個實際的物理文件來表示log group信息,log group 由多個重做日志文件組成,每個log group中的日志文件是相同的,且在InnoDB 1.2版本之前,重做日志文件的總大小要小於4GB,從InnoDB 1.2版本開始重做日志文件的總大小限制提高為512GB,InnoSQL版本的InnoDB存儲引擎在1.1版本就支持大於4GB的重做日志
重做日志文件中存儲就是之前log buffer中保存的log block。因此其也是根據塊的方式進行物理存儲的管理,每個塊的大小與log block一樣,同樣為512字節,在InnoDB存儲引擎運行過程中,log buffer根據一定的規則將內存中的log block刷新到磁盤。這個規則具體是
事務提交時
當log buffer中有一半的內存空間已經被使用時
log checkpoint時
對於log block的寫入追加在redo log file最后部分,當一個redo log file寫滿時,會接着寫下一個redo log file,其使用的方式為round-robin
雖然log block總是在redo log file的最后部分進行寫入,有的讀者可能以為對redo log file的寫入時順序的,其實不是,因為redo log file除了保存log buffer刷新到磁盤的log block,還保存了一些其他的信息,這些信息一共占用2KB大小,即每個redo log file的前2KB的部分不保存log block信息,對於log group中的第一個redo log file,其前2KB的部分保存4個512字節大小的塊,其中存放的內容為
需要特別注意,上述信息僅在每個log group的第一個redo log file中進行存儲,log group中的其余redo log file僅保留這些空間,但不保存上述信息。正因為保存了這些信息,就意味着對redo log file 的寫入並不是完全順序的。因為其除了log block的寫入操作,還需要更新前2KB部分的信息,這些信息對於InnoDB存儲引擎的恢復操作來說非常關鍵和重要,故log group與redo log file 之間的關系如下
在log filer header 后面的部分為InnoDB存儲引擎保存的checkpoint(檢查點)值,其設計時交替寫入。這樣的設計避免了因介質失敗而導致無法找到可用的checkpoint的情況
4 重做日志格式
不同的數據庫操作會有對應的重做日志格式。此外,由於InnoDB存儲引擎的存儲管理是基於頁的,故其重做日志格式也是基於頁的。雖然有着不同的重做日志格式,但他們有着通用的頭部格式,如圖
通用的頭部格式由一下3部分組成
redo_log_type 重做日志類型
space: 表空間ID
page_no 頁的偏移量
之后是redo log body ,根據重做日志類型的不對,會有不同的存儲內容,例如,對於頁上記錄的插入和刪除操作,分別對應的如圖的格式
到InnoDB 1.2版本只是,一共有51中重做日志的類型。隨着功能不斷增加,相信會加入更多的重做日志類型
5 LSN
LSN是Log Sequence Number的縮寫,其代表的是日志序列號,在InnoDB存儲引擎中,LSN占用8個字節,並且單調遞增。LSN的含義
重做日志的寫入的總量
checkpoint的位置
頁的版本
LSN表示事務寫入重做日志字節的總量。例如當前重做日志的LSN為1000,有一個事務T1寫入了100字節的重做日志,那么LSN久變成1100,若又有事務T2寫入200字節的重做日志,那么LSN久變為1300,課件LSN記錄的是重做日志的總量,其單位為字節
LSN不僅記錄在重做日志中,還存在每個頁中,在每個頁的頭部,有一個值FIL_PAGE_LSN,記錄了該頁的LSN,在頁中,LSN表示該頁最后刷新時LSN的大小。因為重做日志記錄的是每個頁的日志,因此頁中的LSN可以判斷頁是否需要進行恢復操作。例如,頁P1的LSN誒10000,而數據庫啟動時,InnoDB檢測到寫入重做日志中的LSN為13000,並且事務已經提交,那么數據庫需要進行恢復操作。將重做日志應用到P1頁中,同樣的,對於重做日志中LSN小於P1頁的LSN,不需要進行重做,因為P1頁中的LSN標示已經被刷新到該位置
用戶可以通過命令SHOW ENGINE INNODB STATUS查看LSN的情況
--- LOG --- Log sequence number 1087932358 Log flushed up to 1087932358 Pages flushed up to 1087932358 Last checkpoint at 1087932358 0 pending log writes, 0 pending chkp writes 8 log i/o's done, 0.00 log i/o's/second
Log sequence number 表示當前的LSN
Log flushed up to 表示刷新到重做日志文件的LSN
Last checkpoint at 表示刷新到磁盤的LSN
雖然在上面的例子中,Log sequence number和Log flushed up to 值是相同的,但是在實際生產環境中,該值可能不同,因為在一個事務中從日志緩沖刷新到重做日志文件並不只是在事務提交時發生,每秒都會有從日志緩沖刷新到重做日志文件的動作,下面是在生成環境下重做日志的信息示例
--- LOG --- Log sequence number 18766833801 Log flushed up to 18766832201 Pages flushed up to 18766816420 Last checkpoint at 18766816420
在生成環境下Log sequence number Log flushed up to Last checkpoint at 三個值可能不同
6 恢復
InnoDB存儲引擎在啟動時不管上次數據運行是否正常關閉,都會嘗試進行恢復操作,因為重做日志記錄的是物理日志,因此恢復的速度比邏輯日志,如二進制日志要快的多,於此同時,InnoDB存儲引擎自身也對恢復進行了一定程度的優化,如順序讀取及並行應用重做日志,這樣可以進一步提高數據庫恢復的速度
由於checkpoint表示已經刷新到磁盤頁上的LSN,因此在恢復過程中僅需恢復checkpoint開始的日志部分。對於圖中的例子,當數據庫在checkpoint的LSN為10 000時發生宕機,恢復操作僅恢復LSN 10000~13000范圍內的日志
InnoDB存儲引擎重做日志是物理日志,因此其恢復的速度較之二進制日志恢復快的多,例如對Insert操作,其記錄的是每個頁的變化,對於下面的表
CREATE TABLE t(a INT ,b INT,PRIMARY KEY(a),key(b));
若執行SQL語句
INSERT INTO t SELECT 1,2;
由於需要對聚集索引頁和輔助索引頁進行操作,其記錄的重做日志大致為
page(2,3),offset 32,value 1,2 #聚集索引 page(2,4),offset 64,value 2 #輔助索引
可以看到記錄的是頁的物理修改曹鄒,若插入涉及B+樹的split,可能會有更多的頁需要記錄日志。此外,由於重做日志是物理日志,因此其是冪等的,冪等的概念如下
f(f(x))=f(x)
但INSERT操作在二進制日志不是冪等,重復執行可能會插入多條重復的記錄,而上述INSERT操作的重做日志是冪等的