系列文章:MySQL系列專欄mysql
經過前面的文章咱們已經瞭解到數據增刪改的一個大體過程以下:web
表空間ID
以及在表空間中的數據頁的頁號
表空間ID+頁號
做爲Key,去緩存頁哈希表
中查找Buffer Pool
是否已經加載了這個緩存頁。若是已經加載了緩存頁,就直接讀取這個緩存頁。Free鏈表
獲取一個空閒頁加入LRU鏈表
中,加載的數據頁就會放到這個空閒的緩存頁中。Flush鏈表
中。LRU鏈表
尾部的冷數據和Flush鏈表
中的髒頁刷盤。這個過程有個最大的問題就是,數據修改且事務已經提交了,但只是修改了Buffer Pool中的緩存頁,數據並無持久化到磁盤,若是此時數據庫宕機,那數據不就丟失了!sql
可是也不可能每次事務一提交,就把事務更新的緩存頁都刷新回磁盤文件裏去,由於緩存頁刷新到磁盤文件裏是隨機磁盤讀寫
,性能是不好的,這會致使數據庫性能和併發能力都不好。數據庫
因此此時就引入了一個 redo log
機制,在提交事務的時候,先把對緩存頁的修改以日誌的形式,寫到 redo log 文件
裏去,並且保證寫入文件成功纔算事務提交成功。並且redo log
是順序寫入
磁盤文件,每次都是追加
到磁盤文件末尾去,速度是很是快的。以後再在某個時機將修改的緩存頁刷入磁盤,這時就算數據庫宕機,也能夠利用redo log
來恢復數據。緩存
這就是MySQL裏常常說到的WAL
技術,WAL 的全稱是Write-Ahead Logging
,它的關鍵點就是先寫日誌,再寫磁盤
。服務器
redo log
本質上記錄的就是對某個表空間的某個數據頁的某個偏移量的地方修改了幾個字節的值,它須要記錄的其實就是 表空間號+數據頁號+偏移量+修改的長度+具體的值,因此 redo log 佔用的空間很是小,一條 redo log 也就幾個字節到幾十個字節的樣子。markdown
針對不對的修改場景,InnoDB定義了多種類型的 redo log,不一樣類型的 redo log 基本上就是下面這樣的一個結構。數據結構
日誌類型就有50多種,其中最簡單的幾種類型就是根據修改了幾個字節的值來劃分的:併發
MLOG_1BYTE
:修改了1字節的值。MLOG_2BYTE
:修改了2字節的值。MLOG_4BYTE
:修改了4字節的值。MLOG_8BYTE
:修改了8字節的值。MLOG_WRITE_STRING
:寫入一串數據。MLOG_WRITE_STRING
類型的 redo log 表示寫入一串數據,可是由於不能肯定寫入的數據佔多少字節,因此須要在日誌結構中添加一個長度字段來表示寫入了多長的數據。高併發
除此以外,還有一些複雜的redo log類型來記錄一些複雜的操做。例如插入一條數據,並不只僅只是在數據頁中插入一條數據,還可能會致使數據頁和索引頁的分裂,可能要修改數據頁中的頭信息(Page Header)、目錄槽信息(Page Directory)等等。
例以下面的一些複雜日誌類型:
MLOG_REC_INSERT
:插入一條非緊湊行格式的記錄的 redo log。MLOG_COMP_REC_INSERT
:插入一條緊湊行格式的記錄的 redo log。MLOG_COMP_REC_DELETE
::刪除一條使用緊湊行格式記錄的 redo log。MLOG_COMP_PAGE_CREATE
:建立一個存儲緊湊行格式記錄的頁面的 redo log。關於日誌格式咱們知道這麼多就好了,對日誌的結構和類型有個大概的認識就能夠了。
一個事務中可能有多個增刪改的SQL語句,而一個SQL語句在執行過程當中可能修改若干個頁面,會有多個操做。
例如一個 INSERT 語句:
若是表沒有主鍵,會去更新內存中的Max Row ID
屬性,並在其值爲256
的倍數時,將其刷新到系統表空間
的頁號爲7
的Max Row ID
屬性處。
接着向聚簇索引插入數據,這個過程要根據索引找到要插入的緩存頁位置,向數據頁插入記錄。這個過程還可能會涉及數據頁和索引頁的分裂,那就會增長或修改一些緩存頁,移動頁中的記錄。
若是有二級索引,還會向二級索引中插入記錄。
最後還可能要改動一些系統頁面,好比要修改各類段、區的統計信息,各類鏈表的統計信息等等。
也就是說一個SQL語句在底層可能會有不少操做,會記錄不少條 redo log
,可是一些操做是不可分割的,是一個原子的。例如向聚簇索引插入記錄,這個操做是不可分割,不能只完成其中一部分。
因此InnoDB將執行語句的過程當中產生的redo log
劃分紅了若干個不可分割的組,一組redo log
就是對底層頁面的一次原子訪問,這個原子訪問也稱爲 Mini-Transaction
,簡稱 mtr
。一個 mtr
就包含一組redo log
,在崩潰恢復時這一組redo log
就是一個不可分割的總體。
一個事務能夠包含若干條SQL語句,每一條SQL語句實際上是由若干個mtr
組成,每個mtr
又能夠包含若干條redo log
,看起來就是下圖所示的結構。
redo log
並非一條一條寫入磁盤的日誌文件中的,並且一個原子操做的 mtr
包含一組 redo log
,一條一條的寫就沒法保證寫磁盤的原子性了。
InnoDB設計了一個 redo log block
的數據結構,稱爲重作日誌塊(block
),重作日誌塊跟緩存頁有點相似,只不過日誌塊記錄的是一條條 redo log。一個 mtr
中的 redo log 其實是先寫到一個地方,而後再將一個 mtr
的日誌記錄複製到block
中,最後在一些時機將block
刷新到磁盤日誌文件中。
一個 redo log block
固定 512字節
大小,由三個部分組成:12字節
的header塊頭,496字節
的body塊體,4字節
的trailer塊尾。redo log 就是存放在 body 塊體中,也就是一個塊實際只有 496字節
用來存儲 redo log。
block header
塊頭記錄了四個信息:
LOG_BLOCK_HDR_NO
:表示塊的惟一編號。
LOG_BLOCK_HDR_DATA_LEN
:表示 block 中已經使用了多少字節,初始值爲12
,由於body
從第12
個字節處開始。若是block body
已經被所有寫滿,那麼本屬性的值就被設置爲512
。
LOG_BLOCK_FIRST_REC_GROUP
:表示block中第一個mtr
日誌組中的第一條 redo log 的偏移量。
LOG_BLOCK_CHECKPOINT_NO
:表示 checkpoint 的序號,後面會介紹。
block trailer
只記錄了一個信息:
LOG_BLOCK_CHECKSUM
:表示block的校驗值。跟 Buffer Pool
相似的,服務器啓動時,就會申請一塊連續的內存空間,做爲 redo log block
的緩衝區也就是 redo log buffer
。而後這片內存空間會被劃分紅若干個連續的 redo log block
,redo log 就是先寫到 redo log buffer 中的 redo log block 中的。
能夠經過啓動參數innodb_log_buffer_size
來指定log buffer
的大小,該參數的默認值爲16MB
。
mysql> SHOW VARIABLES LIKE 'innodb_log_buffer_size';
+------------------------+----------+
| Variable_name | Value |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+
複製代碼
redo log
是以一個 mtr
爲單位寫入 block 中的,多個事務併發執行可能會有多組mtr
,也就是說不一樣事務的 mtr
可能會交叉寫入 block 中。
好比有兩個事務T一、T2:
看起來可能就像下圖這樣,兩個事務中的兩組mtr
交叉寫入block中,每一個mtr的大小也不同,有些大的mtr甚至會佔超出一個block的大小。
圖中還有一個buf_free
,這是InnoDB設計的一個全局變量,用來指向 log buffer 中能夠寫入log的位置。
log block 跟 Buffer Pool 中的緩存頁同樣,會在一些時機刷入磁盤中。
主要有下面的一些時機會刷盤:
若是寫入 log buffer
的日誌佔據了 log buffer
總容量的一半了,默認狀況下也就是超過8MB
的時候,此時就會把他們刷入到磁盤文件裏去。
這種狀況通常在高併發的場景下可能會出現,每秒執行了不少增刪改SQL語句,產生的redo log 瞬間超過了8M
,而後就立馬觸發刷新 log block 到磁盤。不過這種狀況通常比較少。
一個事務提交的時候,必須把它的redo log
都刷入到磁盤文件裏去,只有這樣,才能保證事務的持久性,纔算事務提交成功了(這就是force log at commit
機制,即在事務提交的時候,必須先將該事務的全部事務日誌寫入到磁盤上的日誌文件中進行持久化)。若是在寫入的過程當中MySQL宕機了,那事務也就失敗了。
好比前面的事務T2的 redo log 佔據了3個block,在提交T2事務時,就必須把這3個block都刷入磁盤。
後臺線程刷盤:後臺有一個線程會每隔1秒
,把redo log block
刷到磁盤文件裏去。
MySQL關閉的時候,redo log block
都會刷入到磁盤裏去。
作 checkpoint
的時候。這個後面會說。
須要注意的是,無論什麼時機刷盤,redo log block
始終是順序刷盤
的,好比事務提交的時候,會把這個事務mtr以前的block都刷入磁盤。
好比下面的T一、T2事務,在事務T1提交的時候,雖然事務T2還沒完成,但會把圖中箭頭所指的位置以前的block都刷入磁盤。這個刷盤是時時刻刻都在進行的,因此一次刷盤也不會有不少block。
在提交事務的時候,InnoDB會根據配置的策略來將 redo log 刷盤,這個參數能夠經過 innodb_flush_log_at_trx_commit
來配置。
能夠配置以下幾個值:
0
:事務提交時不會當即向磁盤中同步 redo log,而是由後臺線程來刷。這種策略能夠提高數據庫的性能,但事務的持久性
沒法保證。
1
:事務提交時會將 redo log 刷到磁盤,這能夠保證事務的持久性,這也是默認值。其實數據會先寫到操做系統的緩衝區(os cache),這種策略會調用 fsync
強制將 os cache 中的數據刷到磁盤。
2
:事務提交時會將 redo log 寫到操做系統的緩衝區中,可能隔一小段時間後纔會從系統緩衝區同步到磁盤文件。這種狀況下,若是機器宕機了,而系統緩衝區中的數據還沒同步到磁盤的話,就會丟失數據。
爲了保證事務的持久性
,通常使用默認值,將 innodb_flush_log_at_trx_commit
設置爲1
便可。
MySQL會不停的執行增刪改SQL語句,而後不斷的產生 redo log,那這麼多 redo log 不可能所有存到磁盤文件中。其實也不必,由於 redo log 只是用來恢復數據的,那已經持久化到表空間的數據就不會用 redo log 來恢復了,也就是說可用的 redo log 的量實際上是比較少的。下面來看下 redo log 是如何寫入磁盤文件的。
redo log 會寫入一個目錄下的日誌文件中,實際上是一組日誌文件。
這個目錄默認就是數據目錄,能夠經過以下命令查看:
mysql> SHOW VARIABLES LIKE 'datadir';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| datadir | /var/lib/mysql/ |
+---------------+-----------------+
複製代碼
默認在數據目錄下能夠看到有 ib_logfile0
、ib_logfile1
兩個文件,這就是一組日誌文件。默認一組中有兩個日誌文件,文件名的格式爲 ib_logfile[x]
(x
爲從0
開始的數字)。
咱們能夠經過以下參數來調整 log buffer 的配置:
innodb_log_buffer_size
:指定 redo log buffer 的大小,默認爲 16MB
。innodb_log_group_home_dir
:指定redo log文件所在的目錄,默認值就是當前的數據目錄。innodb_log_file_size
:指定每一個redo log文件的大小,默認值爲48MB
。innodb_log_files_in_group
:指定redo log文件的個數,默認值爲2
,最大值爲100
。mysql> SHOW VARIABLES LIKE 'innodb_log_%';
+-----------------------------+----------+
| Variable_name | Value |
+-----------------------------+----------+
| innodb_log_buffer_size | 16777216 |
| innodb_log_checksums | ON |
| innodb_log_compressed_pages | ON |
| innodb_log_file_size | 50331648 |
| innodb_log_files_in_group | 2 |
| innodb_log_group_home_dir | ./ |
| innodb_log_write_ahead_size | 8192 |
+-----------------------------+----------+
複製代碼
在將 redo log 寫入日誌文件組時,是從 ib_logfile0
開始寫,若是 ib_logfile0
寫滿了,就接着ib_logfile1
寫,ib_logfile1
寫滿了就去寫 ib_logfile2
,依此類推。若是寫到最後一個文件也滿了,就會從新轉到ib_logfile0
覆蓋寫入。
整個過程以下圖所示:
前面已經知道,redo log 是先寫入 redo log buffer 中的 redo log block 中的,而後事務提交時,會將 log block 寫入磁盤中的 redo log 文件。redo log 文件是一組日誌文件,默認在數據目錄下就有兩個 48MB
的日誌文件。
log block 固定爲512字節
大小,redo log 文件也是同樣按512字節
來劃分的,每一個 redo log 文件的格式也是同樣的,都由若干個512字節
的塊組成。
每一個 redo log 文件由兩部分組成:
前2048字節
,也就是前4個block
是用來存儲一些管理信息。其中第1個 block 存儲文件頭信息
,第2個和第4個存儲checkpoint
,第3個block保留未沒用。
從第2048字節日後是用來存儲 redo log block 的。
因此在循環寫日誌文件的時候,實際上是從每一個日誌文件的第2048字節
開始的。但須要注意的是,一組日誌文件中,只有第1個日誌文件的前4個block纔會存儲管理信息,其他的日誌文件只是保留這些空間,不存儲信息。
其中,文件頭信息和兩個checkpoint包含的信息以下圖所示。
header
中的各個屬性:
LOG_HEADER_FORMAT
:redo日誌的版本LOG_HEADER_PAD1
:作字節填充用的,沒什麼實際意義LOG_HEADER_START_LSN
:標記本日誌文件開始的LSN
值,初始值就2048
,指向文件偏移量2048字節
處。LOG_HEADER_CREATOR
:標記本日誌文件的建立者。LOG_BLOCK_CHECKSUM
:本block的校驗值checkpoint
中的各個屬性:
LOG_CHECKPOINT_NO
:服務器作checkpoint
的編號,每作一次checkpoint,該值就加1
。LOG_CHECKPOINT_LSN
:服務器作checkpoint
結束時對應的LSN
值,系統崩潰恢復時將從該值開始。LOG_CHECKPOINT_OFFSET
:上個屬性中的LSN值在redo日誌文件組中的偏移量。LOG_CHECKPOINT_LOG_BUF_SIZE
:服務器在作checkpoint操做時對應的log buffer
的大小。LOG_BLOCK_CHECKSUM
:本block的校驗值。前面已經知道,redo log 是循環寫入日誌文件組中的,那麼就會有個問題,如何保證哪些 redo log 是能夠被覆蓋的呢?redo log 是用來恢復數據的,其實只要 redo log 對應的髒頁已經刷到磁盤了,那這部分 redo log 就沒用了。那恢復數據的時候又應該恢復哪部分數據呢?這一切都和LSN
有關係。
InnoDB設計了一個全局變量 Log Sequence Number
,簡稱 LSN
,就是日誌序列號
的意思。LSN就表明寫入的日誌總量,LSN 的初始值是 8704
,佔用8
個字節,且是單調遞增的。
仍是之前面T一、T2事務爲例,假設T一、T2事務產生的mtr大小以下:
120字節
,mtr_T1_2 200字節
。862字節
,跨了3個block,mtr_T2_2 100字節
。LSN 不只包含 redo log 的大小,還包含了 block 的塊頭和塊尾。下面這張圖就展現了伴隨着T一、T2事務mtr的寫入,LSN的變化狀況。
能夠看出,每一組mtr
都有一個惟一的LSN
值與其對應,LSN 值越小,說明對應mtr
中的redo log
產生的越早。
事務產生的mtr
寫入log block
後,會將修改的髒頁加入到Flush鏈表
頭部,Flush鏈表對應的描述信息塊中會有兩個屬性來記錄LSN信息:
oldest_modification
:記錄mtr開始的LSN值。newest_modification
:記錄mtr結束時的LSN值。接着另外一個mtr寫入後,可能Flush鏈表中已經存在了對應的髒頁,此時會將mtr結束時
的LSN值寫入newest_modification
,本來的oldest_modification
則保持不變。
實際上Flush鏈表
中的髒頁就是按照修改發生的時間順序進行排序,也就是按照oldest_modification
表明的LSN值進行排序的。鏈表靠近尾部的是最先修改的,鏈表頭部則是最新修改的。
前面介紹過數據頁的結構,在它的File Header
中有一個屬性 FIL_PAGE_LSN
,它表示頁面最後被修改時的日誌序列位置LSN
。這個屬性在用 redo log 來恢復數據的時候也起着重要的做用。
在事務中執行增刪改SQL語句時,會更新LRU鏈表
中的緩存頁,而後將這些緩存頁加入Flush鏈表
的頭部,在向log block
中寫入一個mtr
後,就會將最新的LSN值寫入所在頁中的FIL_PAGE_LSN
屬性。
仍是以上面那張T一、T2事務的圖爲例。好比寫入了mtr_T1_1
後,這個mtr
中的 redo logo 相關的緩存頁都會加入 Flush鏈表中,而後這些緩存頁中的FIL_PAGE_LSN
都會更新爲 9448
。在寫入了 mtr_T1_2
後,相關的緩存頁中的FIL_PAGE_LSN
都會更新爲10542
。
回到開頭的問題,刷入磁盤中的哪部分redo log
能夠被覆蓋呢?
redo log 只是爲了系統崩潰後恢復髒頁用的,若是對應的髒頁已經刷新到了磁盤,那麼就算崩潰後也用不着這部分 redo log 了,那麼它佔用的磁盤空間就能夠被覆蓋重用。若是髒頁沒有刷入磁盤,那麼對應的 redo log 就必須保留着。
InnoDB 設計了一個全局變量 checkpoint_lsn
來表明當前系統中能夠被覆蓋的redo log
總量是多少,這個變量初始值也是8704
。當髒頁被刷入磁盤時,就會作一次 checkpoint
來計算 checkpoint_lsn
的值,並寫入 redo log 文件中。
作 checkpoint 主要有兩個步驟:
髒頁只要已經刷入磁盤,那他們對應的redo log就能夠被覆蓋,那如何判斷哪些髒頁已經刷入磁盤呢?
前面說過 Flush鏈表
中的髒頁是按修改時間,也就是oldest_modification
表明的LSN值排序的,鏈表尾部的髒頁就是最先修改的,它所對應的oldest_modification
就是最小的一個LSN值,那這個LSN以前的髒頁就是已經刷入磁盤的。
在作 checkpoint
時,其實就是將Flush鏈表尾部的髒頁的oldest_modification
賦值給checkpoint_lsn
。
接着根據checkpoint_lsn
計算對應的redo log文件日誌偏移量checkpoint_offset
。
InnoDB還設計了一個全局變量checkpoint_no
,表明checkpoint的次數,每作一次checkpoint,這個值就會加1
。
而後就會將這些信息寫入日誌文件組中的第一個日誌文件的checkpoint
中。至於存到 checkpoint1
仍是 checkpoint2
,則根據checkpoint_no
來計算,若是是偶數
,就寫到checkpoint1
,若是是奇數
,就寫入checkpoint2
。
能夠看到checkpoint
中就有三個屬性來存儲這些信息:
checkpoint_no
寫入 LOG_CHECKPOINT_NO
checkpoint_lsn
寫入 LOG_CHECKPOINT_LSN
checkpoint_offset
寫入 LOG_CHECKPOINT_OFFSET
可使用 SHOW ENGINE INNODB STATUS;
命令查看當前InnoDB存儲引擎中的各類LSN值的狀況。
---
LOG
---
Log sequence number 294669958009
Log flushed up to 294669958009
Pages flushed up to 294669957358
Last checkpoint at 294669957349
0 pending log flushes, 0 pending chkp writes
21957055 log i/o's done, 1.98 log i/o's/second
複製代碼
其中的信息以下:
Log sequence number
:表明系統中的LSN
值,也就是當前系統已經寫入的redo log總量。
Log flushed up to
:表明當前系統已經寫入磁盤的redo log量。
Pages flushed up to
:表明Flush鏈表
尾部最先被修改的那個頁面對應的oldest_modification
屬性值。
Last checkpoint at
:當前系統的checkpoint_lsn
值。
例如上面的信息中,Log sequence number
和 Log flushed up to
相等,說明 redo log buffer 中的redo log 都已經刷到 redo log 文件了。可是 Last checkpoint at
小於 Log sequence number
,說明還有一部分髒頁在Flush鏈表
中沒有刷到磁盤。
InnoDB在啓動時無論上次數據庫是否正常關閉,都會嘗試進行恢復操做。若是數據庫是正常關閉,redo log 其實沒什麼用,但若是數據庫宕機,redo log 就能夠用來恢復數據了。
恢復的起點
首先要讀取日誌組中的第一個 redo log 文件頭部的兩個 checkpoint,先比較其中的 checkpoint_no
,哪一個大就使用哪一個 checkpoint。
而後讀取 checkpoint_lsn
,這個值以前的都是已經刷盤了的,但以後的可能刷盤了,也可能沒有刷盤。因此恢復的起點就是 checkpoint_lsn
對應的文件偏移量,從這個偏移量開始讀取 redo log 來恢復頁面。
恢復的終點
redo log block
的頭部header中有一個屬性 LOG_BLOCK_HDR_DATA_LEN
記錄了當前block裏使用了多少字節的空間,對於被寫滿的block來講,該屬性就是512
。若是該屬性的值不爲512,說明這個block還沒寫滿,那終點就是這個block了。
使用哈希表
讀取到內存中的 redo log,並非直接就按順序去重作頁的。而是使用了一個哈希表來加快恢復的速度。
它會根據 redo log 的表空間ID
和頁號
計算出散列值,以此做爲哈希表的 Key,哈希表的 Value 則是一個鏈表,相同表空間ID和頁號的 redo log 就會挨個按順序加入這個鏈表中。
以後就遍歷哈希表來恢復頁,由於對同一個頁面修改的 redo log 都在一個鏈表中,因此能夠一次性將一個頁面修復好(避免了不少讀取頁面的隨機IO),這樣能夠加快恢復速度。
跳過已經刷新到磁盤的頁面
checkpoint_lsn
以前的能夠保證 redo log 對應的髒頁已經刷盤了,可是以後的就不能肯定了。由於在作 checkpoint
以後,可能一些髒頁會不斷的被刷到磁盤中,那這部分 redo log 就不能在頁中重作一遍。
這個時候就會用到前面說過的頁中的FIL_PAGE_LSN
屬性,這個屬性記錄了最近一次修改頁面對應的LSN
值。
若是在作了某次checkpoint
以後有髒頁被刷新到磁盤中,那麼該頁對應的FIL_PAGE_LSN
表明的LSN
值確定大於checkpoint_lsn
的值,對於這種頁面就不須要在應用 redo log 了。