在innodb的引擎實現中,為了實現事務的持久性,構建了重做日志系統。重做日志由兩部分組成:內存日志緩沖區(redo log buffer)和重做日志文件。這樣設計的目的顯而易見,日志緩沖區是為了加快寫日志的速度,而重做日志文件為日志數據提供持久化的作用。在innodb的重做日志系統中,為了更好實現日志的易恢復性、安全性和持久化性,引入了以下幾個概念:LSN、log block、日志文件組、checkpoint和歸檔日志。以下我們分別一一來進行分析。
## 1.LSN
在innodb中的重做日志系統中,定義一個LSN序號,其代表的意思是日志序號。LSN在引擎中定義的是一個dulint_t類型值,相當于uint64_t,關于dulint_t的定義如下:
~~~
typedef struct dulint_struct
{
ulint high; /* most significant 32 bits */
ulint low; /* least significant 32 bits */
}dulint_t;
~~~
LSN真正的含義是儲存引擎向重做日志系統寫入的日志量(字節數),這個日志量包括寫入的日志字節 + block_header_size + block_tailer_size。LSN的初始化值是:LOG_START_LSN(相當于8192),在調用日志寫入函數LSN就一直隨著寫入的日志長度增加,具體看:
~~~
void log_write_low(byte* str, ulint str_len)
{
log_t* log = log_sys;
. . .
part_loop:
/*計算part length*/
data_len = log->buf_free % OS_FILE_LOG_BLOCK_SIZE + str_len;
. . .
/*將日志內容拷貝到log buffer*/
ut_memcpy(log->buf + log->buf_free, str, len);
str_len -= len;
str = str + len;
. . .
if(data_len = OS_FILE_LOG_BLOCK_SIZE - LOG_BLOCK_TRL_SIZE){ /*完成一個block的寫入*/
. . .
len += LOG_BLOCK_HDR_SIZE + LOG_BLOCK_TRL_SIZE;
log->lsn = ut_dulint_add(log->lsn, len);
. . .
}
else /*更改lsn*/
log->lsn = ut_dulint_add(log->lsn, len);
. . .
}
~~~
LSN是不會減小的,它是日志位置的唯一標記。在重做日志寫入、checkpoint構建和PAGE頭里面都有LSN。
**關于日志寫入:**
例如當前重做日志的LSN = 2048,這時候innodb調用log_write_low寫入一個長度為700的日志,2048剛好是4個block長度,那么需要存儲700長度的日志,需要量個BLOCK(單個block只能存496個字節)。那么很容易得出新的LSN = 2048 + 700 + 2 *?LOG_BLOCK_HDR_SIZE(12)?+?LOG_BLOCK_TRL_SIZE(4) = 2776。
**關于checkpoint和日志恢復:**
在page的fil_header中的LSN是表示最后刷新是的LSN, 假如數據庫中存在PAGE1 LSN ?= 1024,PAGE2 LSN = 2048, 系統重啟時,檢測到最后的checkpoint LSN = 1024,那么系統在檢測到PAGE1不會對PAGE1進行恢復重做,當系統檢測到PAGE2的時候,會將PAGE2進行重做。一次類推,小于checkpoint LSN的頁不用重做,大于LSN checkpoint的PAGE就要進行重做。
## 2.Log Block
innodb在日志系統里面定義了log block的概念,其實log block就是一個512字節的數據塊,這個數據塊包括塊頭、日志信息和塊的checksum.其結構如下:

Block no的最高位是描述block是否flush磁盤的標識位.通過lsn可以blockno,具體的計算過程是lsn是多少個512的整數倍,也就是no = lsn / 512 + 1;為什么要加1呢,因為所處no的塊算成clac_lsn一定會小于傳入的lsn.所以要+1。其實就是block的數組索引值。checksum是通過從塊頭開始到塊的末尾前4個字節為止,做了一次數字疊加,代碼如下:
~~~
sum = 1;
sh = 0;
for(i = 0; i < OS_FILE_LOG_BLOCK_SIZE - LOG_BLOCK_TRL_SIZE, i ++){
sum = sum & 0x7FFFFFFF;
sum += (((ulint)(*(block + i))) << sh) + (ulint)(*(block + i));
sh ++;
if(sh > 24)
sh = 0;
}
~~~
在日志恢復的時候,innodb會對加載的block進行checksum校驗,以免在恢復過程中數據產生錯誤。事務的日志寫入是基于塊的,如果事務的日志大小小于496字節,那么會合其他的事務日志合并在一個塊中,如果事務日志的大小大于496字節,那么會以496為長度進行分離存儲。例如:T1 = 700字節大小,T2 = 100字節大小存儲結構如下:

## 3.重做日志結構和關系圖
innodb在重做日志實現當中,設計了3個層模塊,即redo log buffer、group files和archive files。這三個層模塊的描述如下:
redo log buffer ?? ? ?重做日志的日志內存緩沖區,新寫入的日志都是先寫入到這個地方.redo log buffer中數據同步到磁盤上,必須進行刷盤操作。
group files ? ? ?重做日志文件組,一般由3個同樣大小的文件組成。3個文件的寫入是依次循環的,每個日志文件寫滿后,即寫下一個,日志文件如果都寫滿時,會覆蓋第一次重新寫。重做日志組在innodb的設計上支持多個。
archive files ? ? ? ? 歸檔日志文件,是對重做日志文件做增量備份,它是不會覆蓋以前的日志信息。
**以下是它們關系示意圖:**

### 3.1重做日志組
重做日志組可以支持多個,這樣做的目的應該是為了防止一個日志組損壞后,可以從其他并行的日志組里面進行數據恢復。在MySQL-5.6的將日志組的個數設置為1,不允許多個group存在。網易姜承堯的解釋是innodb的作者認為通過外層存儲硬件來保證日志組的完整性比較好,例如raid磁盤。重做日志組的主要功能是實現對組內文件的寫入管理、組內的checkpoint建立和checkpiont信息的保存、歸檔日志狀態管理(只有第一個group才做archive操作).以下是對日志組的定義:
~~~
typedef struct log_group_struct
{
ulint id; /*log group id*/
ulint n_files; /*group包含的日志文件個數*/
ulint file_size; /*日志文件大小,包括文件頭*/
ulint space_id; /*group對應的fil_space的id*/
ulint state; /*log group狀態,LOG_GROUP_OK、LOG_GROUP_CORRUPTED*/
dulint lsn; /*log group的lsn*/
dulint lsn_offset; /*當前lsn相對組內文件起始位置的偏移量 */
ulint n_pending_writes; /*本group 正在執行fil_flush的個數*/
byte**file_header_bufs; /*文件頭緩沖區*/
byte**archive_file_header_bufs;/*歸檔文件頭信息的緩沖區*/
ulint archive_space_id; /*歸檔重做日志ID*/
ulint archived_file_no; /*歸檔的日志文件編號*/
ulint archived_offset; /*已經完成歸檔的偏移量*/
ulint next_archived_file_no; /*下一個歸檔的文件編號*/
ulint next_archived_offset; /*下一個歸檔的偏移量*/
dulint scanned_lsn;
byte* checkpoint_buf; /*本log group保存checkpoint信息的緩沖區*/
UT_LIST_NODE_T(log_group_t) log_groups;
}log_group_t;
~~~
上面結構定義中的spaceid是對應fil0fil中的fil_space_t結構,一個fil_space_t結構可以管理多個文件fil_node_t,關于fil_node_t參見[這里](http://blog.csdn.net/yuanrxdu/article/details/41418421)。
### 3.1.1LSN與組內偏移
?在log_goup_t組內日志模塊當中,其中比較重要的是關于LSN與組內偏移之間的換算關系。在組創建時,會對lsn和對應lsn_offset做設置,假如 初始化為 group lsn = 1024, ?group lsn_offset = 2048,group由3個10240大小的文件組成,LOG_FILE_HDR_SIZE = 2048, 我們需要知道buf lsn = 11240對應的組內offset的偏移是多少,根據log_group_calc_lsn_offset函數可以得出如下公式:
? group_size = 3 * 11240;
?相對組起始位置的LSN偏移 = (buf_ls - group_ls) ?+ log_group_calc_size_offset(lsn_offset ) = (11240 - 1024) - 0 = 10216;
?lsn_offset = log_group_calc_lsn_offset(相對組起始位置的LSN偏移 % group_size) = 10216 + 2 * LOG_FILE_HDR_SIZE = 14312;
這個偏移一定是加上了文件頭長度的。
### 3.1.2 file_header_bufs
file_header_bufs是一個buffer緩沖區數組,數組長度和組內文件數是一致的,每個buf長度是2048。其信息結構如下:

log_group_id ? ? ?對應log_group_t結構中的id
file_start_lsn ? ?當前文件其實位置數據對應的LSN值
File_no ? ? ? ? ? 當前的文件編號,一般在archive file頭中體現
Hot backup str ? ?一個空字符串,如果是hot_backup,會填上文件后綴ibackup。
File_end_ls ? ? ? 文件結尾數據對應的LSN值,一般在archive file文件中體現。
### 3.2 checkpoint
checkpoint是日志的檢查點,其作用就是在數據庫異常后,redo log是從這個點的信息獲取到LSN,并對檢查點以后的日志和PAGE做重做恢復。那么檢查點是怎么生成的呢?當日志緩沖區寫入的日志LSN距離上一次生成檢查點的LSN達到一定差距的時候,就會開始創建檢查點,創建檢查點首先會將內存中的表的臟數據寫入到硬盤,讓后再將redo log buffer中小于本次檢查點的LSN的日志也寫入硬盤。在log_group_t中的checkpoint_buf,以下是它對應字段的解釋:
LOG_CHECKPOINT_NO ? ? ? ? ? ?checkpoint序號,
LOG_CHECKPOINT_LSN ? ? ? ? ? 本次checkpoint起始的LSN
LOG_CHECKPOINT_OFFSET ? ? ? ?本次checkpoint相對group file的起始偏移量
LOG_CHECKPOINT_LOG_BUF_SIZE? redo log buffer的大小,默認2M
LOG_CHECKPOINT_ARCHIVED_LSN? 當前日志歸檔的LSN
LOG_CHECKPOINT_GROUP_ARRAY?? 每個log group歸檔時的文件序號和偏移量,是一個數組
### 3.3 log_t
重做日志的寫入、數據刷盤、建立checkpoint和歸檔操作都是通過全局唯一的,log_sys進行控制的,這是個非常龐大而又復雜的結構,定義如下:
~~~
typedef struct log_struct
{
byte pad[64]; /*使得log_struct對象可以放在通用的cache line中的數據,這個和CPU L1 Cache和數據競爭有和
直接關系*/
dulint lsn; /*log的序列號,實際上是一個日志文件偏移量*/
ulint buf_free; /*buf可以寫的位置*/
mutex_t mutex; /*log保護的mutex*/
byte* buf; /*log緩沖區*/
ulint buf_size; /*log緩沖區長度*/
ulint max_buf_free; /*在log buffer刷盤后,推薦buf_free的最大值,超過這個值會被強制刷盤*/
ulint old_buf_free; /*上次寫時buf_free的值,用于調試*/
dulint old_lsn; /*上次寫時的lsn,用于調試*/
ibool check_flush_or_checkpoint; /*需要日志寫盤或者是需要刷新一個log checkpoint的標識*/
ulint buf_next_to_write; /*下一次開始寫入磁盤的buf偏移位置*/
dulint written_to_some_lsn; /*第一個group刷完成是的lsn*/
dulint written_to_all_lsn; /*已經記錄在日志文件中的lsn*/
dulint flush_lsn; /*flush的lsn*/
ulint flush_end_offset; /*最后一次log file刷盤時的buf_free,也就是最后一次flush的末尾偏移量*/
ulint n_pending_writes; /*正在調用fil_flush的個數*/
os_event_t no_flush_event; /*所有fil_flush完成后才會觸發這個信號,等待所有的goups刷盤完成*/
ibool one_flushed; /*一個log group被刷盤后這個值會設置成TRUE*/
os_event_t one_flushed_event; /*只要有一個group flush完成就會觸發這個信號*/
ulint n_log_ios; /*log系統的io操作次數*/
ulint n_log_ios_old; /*上一次統計時的io操作次數*/
time_t last_printout_time;
ulint max_modified_age_async; /*異步日志文件刷盤的閾值*/
ulint max_modified_age_sync; /*同步日志文件刷盤的閾值*/
ulint adm_checkpoint_interval;
ulint max_checkpoint_age_async; /*異步建立checkpoint的閾值*/
ulint max_checkpoint_age; /*強制建立checkpoint的閾值*/
dulint next_checkpoint_no;
dulint last_checkpoint_lsn;
dulint next_checkpoint_lsn;
ulint n_pending_checkpoint_writes;
rw_lock_t checkpoint_lock; /*checkpoint的rw_lock_t,在checkpoint的時候,是獨占這個latch*/
byte* checkpoint_buf; /*checkpoint信息存儲的buf*/
ulint archiving_state;
dulint archived_lsn;
dulint max_archived_lsn_age_async;
dulint max_archived_lsn_age;
dulint next_archived_lsn;
ulint archiving_phase;
ulint n_pending_archive_ios;
rw_lock_t archive_lock;
ulint archive_buf_size;
byte* archive_buf;
os_event_t archiving_on;
ibool online_backup_state; /*是否在backup*/
dulint online_backup_lsn; /*backup時的lsn*/
UT_LIST_BASE_NODE_T(log_group_t) log_groups;
}log_t;
~~~
### 3.3.1各種LSN之間的關系和分析
從上面的結構定義可以看出有很多LSN相關的定義,那么這些LSN直接的關系是怎么樣的呢?理解這些LSN之間的關系對理解整個重做日志系統的運作機理會有極大的信心。以下各種LSN的解釋:
lsn ? ? ? ? ? ? ? ? ? ? ? ?當前log系統最后寫入日志時的LSN
flush_lsn ? ? ? ? ? ? ? ? ?redolog buffer最后一次數據刷盤數據末尾的LSN,作為下次刷盤的起始LSN
written_to_some_lsn ? ? ? 單個日志組最后一次日志刷盤時的起始LSN
written_to_all_lsn ? ? ? ? 所有日志組最后一次日志刷盤是的起始LSN
last_checkpoint_lsn ? ? ? ?最后一次建立checkpoint日志數據起始的LSN
next_checkpoint_lsn ? ? ? ?下一次建立checkpoint的日志 ? ?數據起始的LSN,用log_buf_pool_get_oldest_modification獲得的
archived_lsn ? ? ? ? ? ? ? 最后一次歸檔日志數據起始的LSN
next_archived_lsn ? ? ? ? ?下一次歸檔日志數據的其實LSN ?
**關系圖如下:**

### 3.3.2偏移量的分析
log_t有各種偏移量,例如:max_buf_free、buf_free、flush_end_offset、buf_next_to_write等。偏移和LSN不一樣,偏移量是相對redo log buf其實位置的絕對偏移量,LSN是整個日志系統的序號。
max_buf_free ? ? ? ?寫入日志是不能超過的偏移位置,如果超過,將強制redo log buf寫入磁盤
buf_free ? ? ?? ? ? 當前日志可以寫的偏移位置
?buf_next_to_write ? 下一次redo log buf數據寫盤的數據起始偏移,在所有刷盤IO完成后,其值和?flush_end_offset是一致的。
flush_end_offset ? ?本次刷盤的數據末尾的偏移量,相當于刷盤時的buf_free,當flush_end_offset 超過max_buf_free的一半時會將未寫入的數據移到 ? ? ? ? ? ? ? ? ? ? ? redobuffer的最前面,這時buf_free和buf_next_to_write都將做調整
大小關系圖如下:

### 3.4內存結構關系圖

## 4.日志寫入和日志保護機制
innodb有四種日志刷盤行為,分別是異步redo log buffer刷盤、同步redo log buffer刷盤、異步建立checkpoint刷盤和同步建立checkpoint刷盤。在innodb中,刷盤行為是非常耗磁盤IO的,innodb對刷盤做了一套非常完善的策略。
### 4.1重做日志刷盤選項
在innodb引擎中有個全局變量srv_flush_log_at_trx_commit,這個全局變量是控制flushdisk的策略,也就是確定調不調用fsync這個函數,什么時候掉這個函數。這個變量有3個值。這三個值的解釋如下:
0 ? ? 每隔1秒由MasterThread控制重做日志模塊調用log_flush_to_disk來刷盤,好處是提高了效率,壞處是1秒內如果數據庫崩潰,日志和數據會丟失。
1 ? ? 每次寫入重做日志后,都調用fsync來進行日志寫入磁盤。好處是每次日志都寫入了磁盤,數據可靠性大大提高,壞處是每次調用fsync會產生大量的磁盤IO,影響數據庫性能。
2 ? ? 每次寫入重做日志后,都將日志寫入日志文件的page cache。這種情況如果物理機崩潰后,所有的日志都將丟失。
### 4.2日志刷盤保護
由于重做日志是一個組內多文件重復寫的一個過程,那么意味日志如果不及時寫盤和創建checkpoint,就有可能會產生日志覆蓋,這是一個我們不愿意看到的。在innodb定義了一個日志保護機制,在存儲引擎會定時調用log_check_margins日志函數來檢查保護機制。簡單介紹如下:
引入三個變量 buf_age、checkpoint_age和日志空間大小. ? ?? ?
? ? ? ? ?buf_age = lsn -oldest_lsn;
? ? ? ? ?checkpoint_age =lsn -?last_checkpoint_lsn;
? ? ? ? 日志空間大小 = 重做日志組能存儲日志的字節數(通過log_group_get_capacity獲得);
當buf_age >=日志空間大小的7/8時,重做日志系統會將red log buffer進行異步數據刷盤,這個時候因為是異步的,不會造成數據操作阻塞。
當buf_age >=日志空間大小的15/16時,重做日志系統會將redlog buffer進行同步數據刷盤,這個時候會調用fsync函數,數據庫的操作會進行阻塞。
? ???
當 checkpoint_age >=日志空間大小的31/32時,日志系統將進行異步創建checkpoint,數據庫的操作不會阻塞。
當?checkpoint_age == 日志空間大小時,日志系統將進行同步創建checkpoint,大量的表空間臟頁和log文件臟頁同步刷入磁盤,會產生大量的磁盤IO操作。數據庫操作會堵塞。整個數據庫事務會掛起。
## 5.總結
????? Innodb的重做日志系統是相當完備的,它為數據的持久化做了很多細微的考慮,它效率直接影響MySQL的寫效率,所以我們深入理解了它便以我們去優化它,尤其是在大量數據刷盤的時候。假設數據庫的受理的事務速度大于磁盤IO的刷入速度,一定會出現同步建立checkpoint操作,這樣數據庫是堵塞的,整個數據庫都在都在進行臟頁刷盤。避免這樣的問題發生就是增加IO能力,用多磁盤分散IO壓力。也可以考慮SSD這讀寫速度很高的存儲介質來做優化。