1. 程式人生 > 其它 >MySQL 基礎(二)日誌

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 中的內容沖刷到磁碟上的操作:

  1. log buffer 的可用空間不足 50% 的時候
  2. 事務提交
  3. 由於後臺執行緒的存在,大約會以每秒一次的頻率將 log buffer 中的內容寫入到磁碟中
  4. 正常關閉伺服器時
  5. 做 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

刪除一條記錄的一般步驟如下:

  1. delete mark 階段,這個階段會將待刪除的記錄的 deleted_flag 標記置為 1,表示這條記錄在邏輯上已經被刪除了,但是此時這條記錄並沒有直接進入到 PAGE_FREE(垃圾連結串列)中,即上一條記錄與這條記錄之間的連結關係依舊存在
  2. purge 階段,當提交 DELETE 事務時,MySQL 會通過專門的執行緒將記錄連結串列中 deleted_flag 標記為 1 的記錄從記錄連結串列中移除出來,然後加入到垃圾連結串列中,作為垃圾連結串列的頭節點

執行刪除操作之後對應的 undo log 的內容可能類似下圖所示:

為了保證能夠恢復資料,通過 roll_pointer 指向對應的刪除的記錄的插入 undo log

對於已經移除的記錄對應的記憶體空間,這部分的記憶體空間是可以被重新使用的:

  • 回憶一下 Page 的結構,在 PagePage Header 中存在一個名為 PAGE_GABAGE 的屬性,該屬性記錄著當前的頁面中可複用的記憶體的總位元組數(即已經移除到垃圾連結串列的所有記錄再當前 Page 中所佔的記憶體總和),每當有記錄從 Page 移動到垃圾連結串列時,都會將對應的記錄所佔的總位元組數加到 PAGE_GABAGE
  • 每當新插入資料時,首先判斷垃圾連結串列的頭節點的記錄的空間大小是否能夠容納新插入的記錄,如果可以容納那麼直接將這條新的記錄放入到原來已經移除的記錄的空間中,再調整記錄連結串列;如果空間不足以容納新插入的記錄,那麼直接申請一塊新的空間來放入這條記錄
  • 如果插入資料時當前的 Page 的可申請空間不足,那麼將會首先會通過 PAGE_GABAGEFree 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 分裂操作,再將插入新的記錄

  • 更新主鍵

    分為以下兩個步驟:

    1. 將舊記錄進行 delete mark 操作,即將該記錄的 deleted_flag 置為 1,只有噹噹前事務提交時,才會有專門的執行緒執行 purge 操作。這是由於可能當前的記錄被多個事務所併發地訪問,直接 purge 將會導致別的事務出現不可預見的問題
    2. 根據更新後各個列的值建立一條新的記錄,並將其插入到聚簇索引中

    針對更新主鍵這種型別的操作,首先在 delete mark 階段會記錄一條 TRX_UNDO_DEL_MARK_REC 的刪除 undo log,然後再插入新的資料時會產生一條 TRX_UNDO_INSERT_REC 的插入 undo log,即,每次對主鍵的更新都會產生兩條 undo log