MySQL 基礎(二)日誌
在作業系統和資料庫管理系統中,為了提高資料的容災性,一般都會通過寫入相關日誌的方式來記錄資料的修改,使得系統受到災難時能夠從之前的資料中恢復過來。MySQL 也提供了日誌的機制來提高資料的容災性,主要包括 redo 日誌和 undo 日誌
redo 日誌
在 Buffer Pool中修改了頁,如果在將 Buffer Pool 中的內容沖洗到磁碟上的這一過程出現了問題,導致記憶體中的資料失效,那麼這個已經提交的事務在資料庫中所做的修改就丟失了,這時需要通過 redo 日誌來重新提交本次的事務。
redo 簡單日誌型別
-
通用日誌型別
具體的結構如下圖所示:
-
固定長度日誌型別
主要有以下幾種:
MLOG_1BYTE(type = 1)、MLOG_2BYTE(type = 2)、MLOG_4BYTE(type = 4)、MLOG_8BYTE(type = 8)
具體的結構如下圖所示:
-
不限定長度日誌型別
對應的型別為 MLOG_WRITE_STRING(type = 30),具體的結構如下圖所示:
redo 複雜日誌型別
複雜日誌的結構如下所示:
存在以下幾種型別:
- MLOG_REC_INSERT(type = 9)(插入記錄,非緊湊型)
- MLOG_COMP_REC_INSERT(type = 38)(插入記錄,緊湊型)
- MLOG_COMP_PAGE_CREATE(type = 58)(建立頁)
- MLOG_COMP_REC_DELETE(type = 42)(刪除記錄)
- MLOG_COMP_LIST_START_DELETE(type = 44)(指定開始位置刪除)
- MLOG_COMP_LIST_END_DELETE(type = 43)(指定結束位置刪除)
- MLOG_ZIP_PAGE_COMPRESS(type = 51)(壓縮頁)
redo 日誌組
通過日誌組來保證事務的一致性,具體的日誌組結構如下圖所示:
通過 MLOG_MULTI_REC_CORD
判斷 redo 日誌的組別,在 redo 時會將該欄位前的所有 redo 日誌視為一個事務中的操作(即再執行事務)。由於一個事務可能會存在多個對資料修改的操作,因此會有多條日誌記錄,簡單的一條 redo 日誌無法保證整個事務的原子性
針對單條 redo 日誌,單獨放在一個日誌組中可能過於浪費空間,為此,對於單條的 redo 日誌,將會從 redo 日誌的 “type” 欄位中剝離一個位來表示該條日誌是否是單條原子性操作,具體地,日誌組中的 redo 日誌的 ”type“ 欄位的結構如下所示:
flag 位為 1,表示該 redo 日誌是一個單條原子性的操作,為 0 則表示一般日誌;通過該 flag 位,同樣可以保證事務的一致性,因此當該 flag 位為 1 時,它將不屬於一個 redo 日誌組
redo 日誌緩衝區
MTR
MTR(Mini—Transaction):對於底層 Page 的一次原子訪問的過程被稱為一個 Mini—Transaction
由於一個事務可以執行多個 SQL
語句,因此一個事務可以包含多個 MTR; MTR 可以包含多條 redo 日誌,具體的對應關係如下圖所示:
redo 日誌塊結構
redo 日誌的塊結構如下圖所示:
具體的關於 log block header 中的相關欄位的介紹如下:
LOG_BLOCK_HDR_NO
:塊編號LOG_BLOCK_HDR_DATA_LEN
:在 log block body 中實際儲存的資料體的長度LOG_BLOCK_FIRST_REC_GROUP
:對應的 MTR(參見 MTR 中 redo log 的對應關係)LOG_BLOCK_CHECKPOINT_NO
:。。。。
這是組成 redo log buffer 的基本單位
redo log buffer
和 Page 類似,在將 redo log 寫入到磁碟中時,不會直接與磁碟互動,而是首先將 redo log 寫入到記憶體中的 buffer 區,再合適的時間通過後臺執行緒再衝洗到磁碟上
redo log buffer 的組成如下圖所示:
日誌的寫入過程:當提交事務時,會將事務的 MTR 分解寫入,當有多個事務併發地提交時,MTR 的寫入順序將是不確定的
以下圖為例,假設現在有兩個事務 T1 和 T2,這兩個事務分別存在兩個 MTR :mtr_t1_1、mtr_t1_2 和 mtr_t2_1、mtr_t2_2,實際寫入情況可能如下圖所示:
值得注意的是,為了保證記憶體的連續性,寫入操作將是順序的,可以看到,mtr_t1_2 由於內容較多,為了保證記憶體的順序性,這會使得mtr_t1_2 會橫跨多個 block 進行寫入,儘管 mtr_t2_2 有機會在這個過程中寫入,但是依舊需要等待來維持順序寫
具體地,一個事務提交時,寫入 redo log 的步驟如下:開始事務——> 執行 SQL ——> 產生 redo log ——> redo log 聚集到 MTR 中 ——> 寫入到 block ——> 寫入到 log buffer
資料沖刷
在滿足以下幾種條件時,將會執行將 log buffer 中的內容沖刷到磁碟上的操作:
- log buffer 的可用空間不足 50% 的時候
- 事務提交
- 由於後臺執行緒的存在,大約會以每秒一次的頻率將 log buffer 中的內容寫入到磁碟中
- 正常關閉伺服器時
- 做 checkpoint 時
MySQL 會將 log buffer 中的內容寫入到名稱為 ib_logfile* 的檔案中,預設情況下,MySQL 會使用使用到兩個檔案,當第一個檔案寫滿時再寫入下一個檔案,當最後一個檔案寫滿時,在寫回到第一個日誌檔案,具體的情況如下圖所示:
# 設定 redo log 檔案的最大大小為 10 MB,設定的值應當在 4MB ~ 512GB 這個範圍內
innodb_log_file_size = 10MB
# 設定 redo log 儲存的檔案的數量,範圍為 2 ~ 100
innodb_log_files_in_group=4
redo log 檔案格式
lsn (log sequence number):用於記錄當前總共已經寫入到 log buffer 的 redo 日誌量,初始值為 8704
redo log 的檔案格式如下圖所示:
首先對於 log file header 部分,關鍵的欄位如下:
LOG_HEADER_START_LSN
:redo 日誌具體內容距離檔案開始位置的偏移量,預設為 2048(512*4)
對於 checkpoint 2 部分,關鍵的是兩個欄位:
LOG_CHECKPOINT_LSN
:checkpoint 在檔案中的偏移量LOG_CHECKPOINT_OFFSET
:checkpoint 在日誌組中的偏移量
undo 日誌
redo log 用於處理容災恢復的操作,主要是為了防止由於受到異常情況導致資料未能真正寫入到磁碟而造成的資料丟失的情況,具體一點,就是說當事務提交時,需要有手段來保證資料的一致性。而 undo log 則是為了處理事務處理時出現異常,需要回滾事務的情況
需要明確的是,undo log 與 redo log 的不同點在於 undo log 的目的在於回滾資料,因此它保留的事務操作是和實際事務操作是相反的。
INSERT 對應的 undo log
使用 TRX_UNDO_INSERT_REC
日誌結構,對應的具體結構如下圖所示:
關鍵的欄位解釋如下:
-
undo no :從 0 開始計數,每生成一條 undo log,則增加這個欄位的值
-
undo Type:該 undo log 所屬的日誌型別,在這裡為
TRX_UNDO_INSERT_REC
-
table id : 在
information_schema.INNODB_TABLES
中可以檢視對應的 Table Id,該欄位值由 MySQL 自動生成 -
主鍵各列資訊:以
<len, value>
組成的對映關係,其中len
表示主鍵欄位型別的長度,value
表示實際值
DELETE 對應的 undo log
刪除操作對應的日誌結構為 TRX_UNDO_DEL_MARK_REC
,具體的結構如下圖所示:
關鍵的欄位解釋如下:
- info bits:記錄頭資訊位元位(不太重要)
- len of index_col_info:索引列每列的欄位長度總和(主鍵索引、二級索引)
- 索引的各列資訊:
pos
表示在記錄中相對於真實記錄資料的開始位置,比如,trx_id
為 1,roller_pointer
為 2
刪除一條記錄的一般步驟如下:
- delete mark 階段,這個階段會將待刪除的記錄的
deleted_flag
標記置為 1,表示這條記錄在邏輯上已經被刪除了,但是此時這條記錄並沒有直接進入到PAGE_FREE
(垃圾連結串列)中,即上一條記錄與這條記錄之間的連結關係依舊存在 - purge 階段,當提交
DELETE
事務時,MySQL 會通過專門的執行緒將記錄連結串列中deleted_flag
標記為 1 的記錄從記錄連結串列中移除出來,然後加入到垃圾連結串列中,作為垃圾連結串列的頭節點
執行刪除操作之後對應的 undo log 的內容可能類似下圖所示:
為了保證能夠恢復資料,通過 roll_pointer
指向對應的刪除的記錄的插入 undo log
對於已經移除的記錄對應的記憶體空間,這部分的記憶體空間是可以被重新使用的:
- 回憶一下
Page
的結構,在Page
的Page Header
中存在一個名為PAGE_GABAGE
的屬性,該屬性記錄著當前的頁面中可複用的記憶體的總位元組數(即已經移除到垃圾連結串列的所有記錄再當前 Page 中所佔的記憶體總和),每當有記錄從Page
移動到垃圾連結串列時,都會將對應的記錄所佔的總位元組數加到PAGE_GABAGE
中 - 每當新插入資料時,首先判斷垃圾連結串列的頭節點的記錄的空間大小是否能夠容納新插入的記錄,如果可以容納那麼直接將這條新的記錄放入到原來已經移除的記錄的空間中,再調整記錄連結串列;如果空間不足以容納新插入的記錄,那麼直接申請一塊新的空間來放入這條記錄
- 如果插入資料時當前的
Page
的可申請空間不足,那麼將會首先會通過PAGE_GABAGE
和Free Space
的總和來判斷是否有足夠的空間來放入這條新的記錄,如果空間足夠,那麼首先建立一個臨時Page
,將當前Page
的內容以及新插入的記錄複製到臨時Page
中,然後再從臨時Page
複製到當前Page
,類似於 “標記—複製” 演算法
UPDATE 對應的 undo log
UPDATE 操作對應的 undo log 日誌型別為 TRX_UNDO_UPD_EXIST_REC
, 對應的結構如下圖所示:
關鍵的幾個欄位的介紹如下:
-
n_updated:本次
UPDATE
語句更新的記錄的數量 -
被更新的列更新前的資訊:
<pos, old_len, old_value>
列表,表示更新列在記錄中的位置,更新前該列佔用的儲存空間的大小,更新前該列的真實值
針對主鍵的更新操作,InnoDB
對於更新主鍵和不更新主鍵這兩種情況有不同的處理方式:
-
不更新主鍵
如果對於本次記錄的更新不會修改主鍵,那麼就會根據更新資料的大小來決定採用 “就地更新” 的方式還是採用 “刪除舊記錄,再插入新紀錄” 的方式
-
就地更新
在更新記錄時,對於被更新的每個列來說,如果更新之後的列與更新前的列佔用的儲存空間一樣大,那麼就可以就地更新,即直接在原有記錄的基礎上完成對應列的修改。這是一種最為理想的方式,最終在
roll_pointer
中引用一條舊資料的更新日誌即可 -
刪除舊記錄,再插入新記錄
如果更新的記錄中有任意一列的大小在更新前後不一致,那麼就需要首先將這條記錄從 Page 中刪除,然後再根據更新列之後的值建立一條新的記錄再插入到 Page 中
值得注意的是,這裡的刪除通過
DELETE
語句來執行刪除操作不同,這裡的刪除操作是直接將這條記錄從記錄連結串列中移動到垃圾連結串列中,沒有設定deleted_flag
這個標記步驟,同時,這裡的刪除也是通過使用者執行緒顯式地來刪除,而不是通過purge
操作時使用的專有執行緒。如果新建立的記錄佔用的空間不超過舊記錄所佔用的空間,那麼就可以直接重用原有的舊記錄使用到的空間而無需顯式地去請求分配記憶體空間;如果新紀錄佔用的空間超過了原有記錄佔用的記憶體空間,那麼就需要在 Page 中新申請一塊空間供新紀錄使用;如果當前 Page 已經沒有足夠的可用空間,那麼就需要執行 Page 分裂操作,再將插入新的記錄
-
-
更新主鍵
分為以下兩個步驟:
- 將舊記錄進行 delete mark 操作,即將該記錄的
deleted_flag
置為 1,只有噹噹前事務提交時,才會有專門的執行緒執行purge
操作。這是由於可能當前的記錄被多個事務所併發地訪問,直接purge
將會導致別的事務出現不可預見的問題 - 根據更新後各個列的值建立一條新的記錄,並將其插入到聚簇索引中
針對更新主鍵這種型別的操作,首先在 delete mark 階段會記錄一條
TRX_UNDO_DEL_MARK_REC
的刪除 undo log,然後再插入新的資料時會產生一條TRX_UNDO_INSERT_REC
的插入 undo log,即,每次對主鍵的更新都會產生兩條 undo log - 將舊記錄進行 delete mark 操作,即將該記錄的