- 事務隔離性可以使用前面介紹的鎖來實作。原子性、一緻性、持久性通過資料庫的redo log和undo log來完成:
- redo log:稱為重做日志。用來保證事務的原子性和持久性
- undo log:用來保證事務的一緻性
- redo和undo的作用都可以視為一種恢複操作:
- redo恢複送出事務修改的頁操作
- undo復原行記錄到某個特定版本
- 是以兩者記錄的内容也不同:
- redo通常是實體日志,記錄的是頁的實體修改操作
- undo是邏輯日志,根據每行記錄進行記錄
一、redo基本概述
- 重做日志用來實作事務的持久性,即事務ACID中的D。其由兩部分組成:
- 一是記憶體中的重做日志緩沖(redo log buffer),其是易失的
- 二是重做日志檔案(redo log file),其是持久的
工作原理
- InnoDB事務的存儲引擎,其通過Force Log at Commit機制實作事務的持久性,即當事務送出(commit)時,必須先将該事務的所有日志寫入重做日志檔案進行持久化,待事務的COMMIT操作完成才算完成
- 這裡的日志是指重做日志,在InnoDB存儲引擎中,由兩部分組成:即redo log和undo log
- redo log:用來保證事務的持久性。基本上都是順序寫的,在資料庫運作時不需要對redo log的檔案進行讀取操作
- undo log:用來幫助事務復原及MVCC的功能。是需要進行随機讀寫的
fsync操作
- 為了確定每次日志都寫入重做日志檔案,在每次将重做日志緩沖寫入重做日志檔案後,InnoDB都需要調用一次fsync操作
- 由于重做日志檔案打開并沒有使用O_DIRECT選項,是以重做日志緩沖先寫入檔案系統緩存。為了確定重做日志寫入磁盤,必須進行一次fsync操作
- 由于fsync的效率取決于磁盤的性能,是以磁盤的性能決定了事務送出的性能,也就是資料庫的性能
- InnoDB允許使用者手工設定非持久的情況發生,以此提高資料庫的性能。即當事務送出時,日志不寫入重做日志檔案,而是等待 一個事件周期後再執行fsync操作。由于并非強制在事務送出時進行一次fsync操作,顯然這可以顯著提高資料庫的性能。但是當資料庫發生當機時,由于部分日志未重新整理到磁盤,是以會丢失最後一段事件的事務(具體見下面的innodb_flush_log_at_trx_commit參數)
innodb_flush_log_at_trx_commit參數
- 該參數用來控制重做日志重新整理到磁盤的政策
- 取值如下:
- 0:表示事務送出時不進行寫入重做日志操作,這個操作僅在master thread中完成,而master thread中每1秒會進行一次重做日志檔案的fsync操作
- 1(預設值):表示事務送出時必須調用一次fsync操作
- 2:表示事務送出時将重做日志寫入重做日志檔案,但僅寫入檔案系統的緩存中,不進行fsync操作。在這個設定下,當MySQL資料庫發生當機而作業系統不發生當機時,并不會導緻事務的丢失。而當作業系統當機時,重新開機資料庫會丢失未從檔案系統緩存重新整理到重做日志檔案那部分事務
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志)) 示範案例
- 下面看一個例子,在不同的參數值下資料庫的工作效率
- 建立一個表test_load和一個存儲過程p_load:
- 存儲過程的作用是将資料不斷地插入表中,并且每插入一條就進行是以顯式的commit操作
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 ;
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志))
- innodb_flush_log_at_trx_commit的值為1時:執行下面的指令,向表中插入50萬行的記錄,并執行50萬次的fsync操作:
call p_load(500000);
- 看到插入50萬條記錄差不多需要2分鐘的時間,在實際生産環境中這個時間是使用者不能接受的(時間長的主要原因就是fsync操作所需的時間)
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志))
- innodb_flush_log_at_trx_commit的值為0時:删除表中的資料,然後再向表中插入50萬行的記錄,結果如下:
- 看到插入50萬條記錄隻需要8秒左右
delete from test_load;
set global innodb_flush_log_at_trx_commit=0;
show variables like 'innodb_flush_log_at_trx_commit';
call p_load(500000);
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志))
- innodb_flush_log_at_trx_commit的值為2時:删除表中的資料,然後再向表中插入50萬行的記錄,結果如下:
- 看到插入50萬條記錄差不多需要14秒左右
delete from test_load;
set global innodb_flush_log_at_trx_commit=2;
show variables like 'innodb_flush_log_at_trx_commit';
call p_load(500000);
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志))
- 由此可以看出,fsync的操作減少,資料庫執行的性能也提高。但是将innodb_flush_log_at_trx_commit設定為0或2來提高事務送出的性能,但是卻喪失了事務的ACID特性
- 對于上面的存儲過程,為了提高事務的送出性能,應該在将50萬行記錄插入表後進行一次總的COMMIT操作,而不是在每插入一條記錄後就進行一次COMMIT。這樣做的好處是還可以使事務復原時復原到事務最開始的确定狀态
二、重做日志與二進制日志的不同
- 二進制日志介紹參閱:MySQL(InnoDB剖析):11---檔案之(日志檔案:錯誤日志(error log)、慢查詢日志(slow query log)、查詢日志(query log)、二進制日志(bin log))_董哥的黑闆報的部落格-
- 二進制日志其用來進行POINT-IN-TIME(PIT)的恢複以及主從複制(Replication)環境的建立。從表面上看其和重做日志非常相似,都是記錄對于資料庫操作的日志。然而,從本質上看,兩者有着非常大的不同
不同點①
- 日志是在InnoDB存儲引擎層産生
- 二進制日志是在MySQL資料庫的上層産生的,并且二進制日志不僅僅針對于InnoDB而言,MySQL資料庫中的任何存儲引擎對于資料庫的更改都會産生二進制日志
不同點②
- 兩種日志記錄的内容形式不同:
- 二進制日志是一種邏輯日志,其記錄的是對應的SQL語句
- 重做日志是實體格式日志,其記錄的是對于每個頁的修改
不同點③
- 兩種日志記錄寫入磁盤的時間點不同
- 二進制日志隻在事務送出完成後進行一次寫入
- 重做日志在事務進行中不斷地被寫入,這表現為日志并不是随事務送出的順序寫入的
- 從下圖可以看出:
- 二進制日志僅在事務送出時記錄,并且對于每一個事務,僅包含對應事務的一個日志
- 重做日志其記錄的實體記錄檔,是以每個事務對應多個日志條目,并且事務的重做日志寫入是并發的,并非在事務送出時寫入,故其在檔案中記錄的順序并非是事務開始的順序(下圖中帶有*的,意為該事務的送出)
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志))
三、重做日志塊(log block)
- 在InnoDB中,重做日志都是以512位元組進行存儲的
- 這意味着重做日志緩存、重做日志檔案都是以塊(block)的方式進行儲存的,稱之為重做日志塊(redo log block),每塊的大小為512位元組
- 若一個頁中産生的重做日志數量大于512位元組,那麼需要分割為多個重做日志塊進行存儲。此外,由于重做日志塊的大小和磁盤扇區大小一樣,都是512位元組,是以重做日志的寫入可以保證原子性,不需要doublewrite技術(doublewrite技術參閱:MySQL(InnoDB剖析):08---InnoDB關鍵特性(插入緩沖(Insert Buffer)、兩次寫(doublewrite)、自适應哈希索引(AHI)、異步IO(AIO)、重新整理鄰接頁)_董哥的黑闆報的部落格-
重做日志塊結構
- 重做日志塊除了日志本身之外,還由日志塊頭(log block header)以及日志塊尾(log block tailer)兩部分組成
- 重做日志頭一共占用12位元組
- 重做日志尾占用8位元組
- 故每個重做日志塊實際可以存儲的大小為492位元組(512-12-8)
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志)) 重做日志緩存結構
- 下圖顯示了重做日志緩存的結構,由每個為512位元組大小的日志塊所組成
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志)) 重做日志頭(log block header)
- 重做日志頭一共占用12位元組,由4部分組成,如下圖所示:
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志)) ①LOG_BLOCK_HDR_NO
- 該标記用來辨別這個塊(log block)位于重做日志塊緩存數組中的位置
- 其實遞增并且循環使用的,占用4位元組,由于第一位用來判斷是否有flush bit,是以最大的值為2G
②LOG_BLOCK_HDR_DATA_LEN
- 占用2位元組,表示該塊所占用的大小
- 當log block被寫滿時,該值為0x200,表示使用全部log block空間,即占用512位元組
③LOG_BLOCK_FIRST_REC_GROUP
- 占用2位元組,表示log block中第一個日志所在的偏移量
- 如果該值的大小和LOG_BLOCK_HDR_DATA_LEN相同,則表示目前log block不包含新的日志
- 若事務T1的重做日志1占用792位元組,事務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為282(270+12)
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志)) ④LOG_BLOCK_CHECKPOINT_NO
- 占用4位元組,表示該log block最後被寫入時的檢查點第4位元組的值
重做日志尾(log block tailer)
- 重做日志尾占用8位元組,由1部分組成,如下圖所示:
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志)) ①LOG_BLOCK_TRL_NO
- 其值和LOG_BLOCK_HDR_NO相同,并在函數lob_block_init中被初始化
四、重做日志組(log group)
- log group為重做日志組,其中有多個重做日志檔案。雖然源碼中已經支援log group的鏡像功能,但是在ha_innodbase.cc檔案中進制了該功能,是以InnoDB存儲一你請實際隻有一個log group
- log group是一個邏輯上的概念,并沒有一個實際存儲的實體檔案來表示log group資訊
重做日志檔案
- log group由多個重做日志檔案組成,每個log group中的日志檔案大小是相同的:
- 且在InnoDB 1.2版本之前,重做日志檔案的總大小要小于4GB(不能等于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位元組大小的塊,其中存放的内容如下圖所示:
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志))
- 需要注意的是:
- 上述資訊僅在每個log group的第一個redo log file中進行存儲,log group中的其餘redo log file僅保留這些空間,但不儲存上述資訊
- 正因為儲存了這些資訊,就意味着對redo log file的寫入并不是完全順序的。因為其除了log block的寫入操作,還需要更新前2KB部分的資訊,這些資訊對于InnoDB的恢複操作來說非常關鍵和重要
- log group與redo log file之間的關系如下圖所示:
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志))
- 在log file header後面的部分為InnoDB儲存的checkpoint(檢查點)值,其設計是交替寫入,這樣的設計避免了因媒體失敗而導緻無法找到可用的checkpoint的情況
五、重做日志格式
- 上面介紹的是重做日志的存儲格式,下面介紹的是重做日志的内容格式
- 不同的資料庫操作會有對應的重做日志格式。此外,由于InnoDB的存儲管理是基于頁的,故其重做日志格式也是基于頁的
- 雖然有着不同的重做日志格式,但是他們有着通用的頭部結構:
- 通用的頭部格式由以下3部分組成:
- redo_log_type:重做日志的類型
- space:表空間的ID
- page_no:頁的偏移量
- 之後的redo log body的部分,根據重做日志類型的不同,會有不同的存儲内容。例如,對于頁上記錄的插入和删除操作,分别對應下面所示的格式:
- 到InnoDB 1.2版本時,一共有51種重做日志類型。随着功能不斷地增加,相信會加入越來越多的重做日志類型
六、LSN
- LSN是Log Sequence Number的縮寫,其代表的是日志序列号。在InnoDB存儲引擎中,LSN占用8位元組,并且單調遞增
- LSN表示的含義有:
- 重做日志寫入的總量
- checkpoint的位置
- 頁的版本
重做日志寫入的總量
- LSN表示事務寫入重做日志的位元組的總量
- 例如:目前重做日志的LSN為1000,有一個事務T1寫入了100位元組的重做日志,那麼LSN就變為了1100,若有事務T2寫入了200位元組的重做日志,那麼LSN就變為了1300
- 可見LSN記錄的是重做日志的總量,其機關為位元組
checkpoint的位置
- LSN不僅記錄在重做日志中,還存在于每個頁中。在每個頁的頭部,有一個值FIL_PAGE_LSN,記錄了該頁的LSN
- 在頁中,LSN表示該頁最後重新整理時LSN的大小
- 因為重做日志記錄的是每個頁的日志,是以頁中的LSN用來判斷頁是否需要進行恢複操作。例如:
- 頁P1的LSN為10000,而資料庫啟動時,InnoDB檢測到寫入重做日志中的LSN為13000,并且該事務已經送出,那麼資料庫需要進行恢複操作,将重做日志應用與P1頁中
- 同樣的,對于重做日志中LSN小于P1頁的LSN,不需要進行重做,因為P1頁中的LSN表示頁已經被重新整理到該位置
檢視LSN
- 使用者可以通過下面的指令檢視LSN的情況:
show engine innodb status\G
- Log sequence number:表示目前的LSN
- Log flushed up to:表示重新整理到重做日志檔案的LSN
- Last checkpoint at:表示重新整理帶磁盤的LSN
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志))
- 雖然在上面的例子中,Log sequence number和Log flushed up to的值是相同的大,但是在實際生産環境中,該值有可能是不同的。因為在一個事務中從日志緩沖重新整理到重做日志檔案并不隻是在事務送出時發生,每秒都會有從日志緩沖重新整理到重做日志檔案的東西。下面是在生産環境下重做日志的資訊的執行個體
![]()
MySQL(InnoDB剖析):39---事務之(事務的實作:redo log(重做日志))
- 可以看到,在生産環境下Log sequence number、Log flushed up to、Last checkpoint at三個值可能是不同的
七、恢複
- InnoDB在啟動時不管上次資料庫運作時是否正常關閉,都會嘗試進行恢複操作
- 因為重做日志記錄的是實體日志,是以恢複的速度比邏輯日志(如二進制日志)要快很多。與此同時,InnoDB自身也會恢複進行了一定程度的優化,如順序讀取及并行應用重做日志,這樣可以進一步地提高資料庫恢複的速度
- 由于checkpoint表示已經重新整理到磁盤頁上的LSN,是以在恢複過程中僅需恢複checkpoint開始的日志部分
- 對于下圖所示的例子,當資料庫在checkpoint的LSN為10000時發生當機,恢複操作僅恢複LSN 10000~13000範圍内的日志
- InnoDB的重做日志是實體日志,是以其恢複速度較之二進制日志恢複快得多。例如對于INSERT操作,其記錄的是每個頁上的變化。對于下面的表:
create table t(
a int,
b int,
primary key(a),
key(b)
);
- 若執行SQL語句
insert into t select 1,2;
- 由于需要聚集索引和輔助索引頁進行操作,其記錄的重做日志大緻為:
- 可以看到記錄的是頁的實體修改操作,若插入設計B+樹的split,可能會有更多的頁需要記錄日志。此外,由于重做日志是實體日志,是以其是幂等的。幂等的概念如下:
- 有的DBA或開發人員錯誤的認為隻要将二進制日志的格式設定為ROW,那麼二進制日志也是幂等的。這顯然是錯誤的,舉個簡單的例子,INSERT操作在二進制日志中就不是幂等的,重複執行可能會插入多條重複的記錄。而上述INSERT操作的重做日志是幂等的