MySQL中的redo log和undo log

pedro7 發表於 2021-07-26
MySQL

MySQL中的redo log和undo log

MySQL日誌系統中最重要的日誌為重做日誌redo log歸檔日誌bin log,後者為MySQL Server層的日誌,前者為InnoDB儲存引擎層的日誌。

1 重做日誌redo log

1.1 什麼是redo log

redo log用於保證事務的永續性,即ACID中的D。

永續性:指一個事務一旦被提交,它對資料庫中資料的改變就是永久性的,接下來即使資料庫發生故障也不應該對其有任何影響。

redo log有兩種型別,分別為物理重做日誌和邏輯重做日誌。在InnoDB中redo log大多數情況下是一個物理日誌,記錄資料頁面的物理變化(實際的資料值)。

1.2 redo log的功能

redo log的主要功能是用於資料庫崩潰時的資料恢復。

1.3 redo log的組成

redo log可以分為以下兩部分

  • 儲存在記憶體中的重做日誌緩衝區
  • 儲存在磁碟上的重做日誌檔案
image-20210725135142321

1.4 記錄redo log的時機

  • 完成資料的修改之後,髒頁刷入磁碟之前寫入重做日誌緩衝區。即先修改,再寫入。

    髒頁:記憶體中與磁碟上不一致的資料(並不是壞的!)

  • 在以下情況下,redo log由重做日誌緩衝區寫入磁碟上的重做日誌檔案。

    • redo log buffer的日誌佔據redo log buffer總容量的一半時,將redo log寫入磁碟。
    • 一個事務提交時,他的redo log都刷入磁碟,這樣可以保證資料絕不丟失(最常見的情況)。注意這時記憶體中的髒頁可能尚未全部寫入磁碟。
    • 後臺執行緒定時重新整理,有一個後臺執行緒每過一秒就將redo log寫入磁碟。
    • MySQL關閉時,redo log都被寫入磁碟。

    第一種情況和第四種情況一定會執行redo log的寫入,第二種情況和第三種情況的執行要根據引數innodb_flush_log_at_trx_commit的設定值,在下文會有詳細描述。

  • 索引的建立也需要記錄redo log。

1.5 一個重做全過程的示例

image-20210725111033589

以更新事務為例。

  1. 將原始資料讀入記憶體,修改資料的記憶體副本。
  2. 生成redo log並寫入重做日誌緩衝區,redo log中儲存的是修改後的新值。
  3. 事務提交時,將重做日誌緩衝區中的內容重新整理到重做日誌檔案。
  4. 隨後正常將記憶體中的髒頁刷回磁碟。

1.6 永續性的保證

1.6.1 Force Log at Commit機制

Force Log at Commit機制實現了事務的永續性。在記憶體中操作時,日誌被寫入重做日誌緩衝區。但在事務提交之前,必須首先將所有日誌寫入磁碟上的重做日誌檔案。

為了確保每個日誌都寫入重做日誌檔案,必須使用一個fsync系統呼叫,確保OS buffer中的日誌被完整地寫入磁碟上的log file。

fsync系統呼叫:需要你在入參的位置上傳遞給他一個fd,然後系統呼叫就會對這個fd指向的檔案起作用。fsync會確保一直到寫磁碟操作結束才會返回,所以當你的程式使用這個函式並且它成功返回時,就說明資料肯定已經安全的落盤了。所以fsync適合資料庫這種程式。

image-20210725112513639
1.6.2 innodb_flush_log_at_trx_commit引數

InnoDB提供了一個引數innodb_flush_log_at_trx_commit控制日誌重新整理到磁碟的策略。

  • innodb_flush_log_at_trx_commit值為1時(預設)。事務每次提交都必須將log buffer中的日誌寫入os buffer並呼叫fsync()寫入磁碟中。
    • 這種方式即使系統崩潰也不會丟失任何資料,但是因為每次提交都寫入磁碟,IO效能較差。
  • innodb_flush_log_at_trx_commit值為0時。事務提交時不將log buffer寫入到os buffer,而是每秒寫入os buffer並呼叫fsync()寫入到log file on disk中。
    • 這實際上相當於在記憶體中維護了一個使用者設計的緩衝區,它減少了和os buffer之間的資料傳輸,有更好的效能。
    • 每秒寫入磁碟,系統崩潰會丟失1s的資料。
  • innodb_flush_log_at_trx_commit值為2時。每次提交都僅寫入os buffer,然後每秒呼叫fsync()將os buffer中的日誌寫入到log file on disk中。
    • 雖然說我們是每秒呼叫fsync()將os buffer中的日誌寫入到log file on disk中,但是平時即使不呼叫fsync,資料也會2自主地逐漸進入磁碟。所以當發生系統崩潰,相比第二種情況,會丟失較少的資料。
    • 但同時,由於每次提交都寫入os buffer,所以相比第二種情況,效能會差一些,但還是比第一種好的。
  • 無論是哪種情況
image-20210725113246802
1.6.3 一個小的效能測試

幾個選項之間的效能差距是極大的,下面做一個簡單的測試。

#建立測試表
drop table if exists test_flush_log;
create table test_flush_log(id int,name char(50))engine=innodb;

#建立插入指定行數的記錄到測試表中的儲存過程
drop procedure if exists proc;
delimiter $$
create procedure proc(i int)
begin
    declare s int default 1;
    declare c char(50) default repeat('a',50);
    while s<=i do
        start transaction;
        insert into test_flush_log values(null,c);
        commit;
        set s=s+1;
    end while;
end$$
delimiter ;

下面均插入十萬條記錄。

Ⅰ 當innodb_flush_log_at_trx_commit值為1時

test> call proc(100000)
[2021-07-25 13:22:02] completed in 27 s 350 ms

需要長達27.35s。

Ⅱ 當innodb_flush_log_at_trx_commit值為2時

test> set @@global.innodb_flush_log_at_trx_commit=2;    
test> truncate test_flush_log;

test> call proc(100000)
[2021-07-25 13:27:33] completed in 5 s 774 ms

只需5.774s,效能大大提升。

Ⅲ 當innodb_flush_log_at_trx_commit值為0時

test> set @@global.innodb_flush_log_at_trx_commit=0;
test> truncate test_flush_log;

test> call proc(100000)
[2021-07-25 13:30:34] completed in 3 s 537 ms

只需3.537s,效能更高。

顯然,innodb_flush_log_at_trx_commit值為1時效能差得非常明顯,改為0和2後效能都有大幅提升,其中0更快但相比2提升不大。

雖然改為0和2可以大幅提升效能,但會嚴重影響安全性。我們可以通過修改儲存過程,將事務的建立和提交放到迴圈外,統一提交,減少了IO頻率。

drop procedure if exists proc;
delimiter $$
create procedure proc(i int)
begin
    declare s int default 1;
    declare c char(50) default repeat('a',50);
    start transaction;
    while s<=i DO
        insert into test_flush_log values(null,c);
        set s=s+1;
    end while;
    commit;
end$$
delimiter ;
1.6.4 迷你事務mini-transaction

mini-trasaction是InnoDB處理小型事務時使用的一種機制,它可以確保併發事務操作和資料庫異常發生時,資料頁中的資料一致性。

迷你事務必須遵循下面三個協議:

  • FIX規則。寫時必須使用獨佔鎖,讀時必須使用共享鎖。反正就是要鎖住。

  • 預寫日誌。預寫日誌即WAL,Write-Ahead Log。持久化資料之前,必須先持久化記憶體中的日誌。每個頁面都有一個LSN(日誌序列號)。在將資料寫入磁碟前,要先將記憶體中序列號小於LSN的日誌寫入磁碟。WAL提供三種持久化模式

    • 最嚴格的是full-sync,fsync保證在返回之前將記錄重新整理到磁碟,最大化了資料的安全性。
image-20210725140700635
  • 第二個級別是write-only,保證記錄寫入作業系統。這允許資料在程式級別的崩潰後倖存。
image-20210725161450627
  • 最不嚴格的是no-sync,將記錄儲存在記憶體緩衝區中,不保證立即寫入檔案系統。
image-20210725161701120
  • 強制日誌再提交。即Force-log-at-commit,它要求提交事務時必須把所有迷你事務日誌重新整理到磁碟。

1.7 寫redo log的過程

image-20210725163751479

如上圖,展示了redo log是如何被寫入log buffer的。每個mini-trasaction對應於每個DML操作,例如更新語句等。

  • 每個資料修改後被寫入迷你事務私有緩衝區。
  • 當更新語句完成,redo log從迷你事務私有緩衝區被寫入記憶體中的公共日誌緩衝區。
  • 提交外部事務時,會將重做日誌緩衝區刷入重做日誌檔案。

1.8 日誌塊 log block

redo log以塊為單位進行儲存,每個塊大小為512位元組。無論是在記憶體重做日誌緩衝區、作業系統緩衝區還是重做日誌檔案中,都是以這樣的512位元組大小的塊進行儲存的。

image-20210726125546331

每個日誌塊頭由以下四個部分組成

  • log_block_hdr_no:(4位元組)該日誌塊在redo log buffer中的位置ID。
  • log_block_hdr_data_len:(2位元組)該log block中已記錄的log大小。寫滿該log block時為0x200,表示512位元組。
  • log_block_first_rec_group:(2位元組)該log block中第一個log的開始偏移位置。
  • lock_block_checkpoint_no:(4位元組)寫入檢查點資訊的位置。

1.9 log group

log group代表redo log的分組,由多個大小相同的redo log file組成。由一個引數innodb_log_files_group決定,預設為2。
[外鏈圖片轉存失敗,源站可能有防盜img-qAyaSeL3543740G:61311akw89MySQL[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-h01w68EG-1627284031849)(G:\markdown\MySQL\image-20210726131134489.png)].png)]

這個group是邏輯上的概念,但可以通過變數 innodb_log_group_home_dir 來定義組的目錄,redo log file都放在這個目錄下,預設是在datadir下。

image-20210726131134489

2 撤銷日誌undo log

2.1 關於undo log

  • undo log存在的意義是確保資料庫事務的原子性。

    原子性是指事務是一個不可分割的工作單位,事務中的操作要麼都發生,要麼都不發生。

  • redo log記錄了事務的行為,可以很好地保證一致性,對資料進行“重做”操作。但事務有時還需要進行“回滾”操作,這時就需要undo log。當我們對記錄做了變更操作的時候就需要產生undo log,其中記錄的是老版本的資料,當舊事務需要讀取資料時,可以順著undo鏈找到滿足其可見性地記錄。

  • undo log通常以邏輯日誌的形式存在。我們可以認為當delete一條記錄時,undo log會產生一條對應的insert記錄,反之亦然。當update一條記錄時,會產生一條相反的update記錄。

  • undo log採用段segment的方式來記錄,每個undo操作在記錄的時候佔用一個undo log segment。

  • undo log也會產生redo log,因為undo log也要實現永續性保護。

2.2 undo log segment

為了保證事務併發操作時,寫各自的undo log時不發生衝突,nnodb用段的方式管理undo log。rollback segment稱為回滾段,每個回滾段中有1024個undo log segment。MySQL5.5以後的版本支援128個rollback segment,就可以儲存128*1024個操作,還可以通過innodb_undo_logs引數定義盯梢個rollback segment。

image-20210726135835836

2.3 purge

在聚集索引列的操作中,MySQL是這樣設計的。對一條delete語句

delete from t where a = 1

假如a有聚集索引(主鍵),那麼不會進行真正的刪除,而是在主鍵列等於1的記錄處設定delete flag為1,即把記錄儲存在B+樹中。同理,對於update操作,不是直接更新記錄,而是把舊紀錄標識為刪除,再建立一條新記錄。

那麼,舊版本記錄什麼時候真正的刪除呢?

InnoDB使用undo日誌進行舊版本的刪除操作,這個操作稱為purge操作。InnoDB開闢了purge執行緒進行purge操作,並且可以控制purge執行緒的數量,每個purge執行緒每10s 進行一次purge操作。

InnoDB的undo log設計

一個頁上允許多個事務的undo log存在,undo log的儲存順序是隨時的。InnoDB維護了一個history連結串列,按照事務提交的順序將undo log進行連線。

image-20210726142109535

在執行purge過程中,InnoDB儲存引擎首先從history list中找到第一個需要被清理的記錄,這裡為trx1,清理之後InnoDB儲存引擎會在trx1所在的Undo page中繼續尋找是否存在可以被清理的記錄,這裡會找到事務trx3,接著找到trx5,但是發現trx5被其他事務所引用而不能清理,故再去history list中取查詢,發現最尾端的記錄時trx2,接著找到trx2所在的Undo page,依次把trx6、trx4清理,由於Undo page2中所有的記錄都被清理了,因此該Undo page可以進行重用。

InnoDB儲存引擎這種先從history list中找undo log,然後再從Undo page中找undo log的設計模式是為了避免大量隨機讀操作,從而提高purge的效率。

3 InnoDB的恢復操作

3.1 資料頁刷盤的規則和checkpoint

記憶體中(buffer pool)未刷到磁碟的資料稱為髒資料(dirty data)。由於資料和日誌都以頁的形式存在,所以髒頁表示髒資料和髒日誌。

在InnoDB中,checkpoint是資料刷盤的唯一規則。checkpoint觸發後,會將記憶體中的髒資料刷到磁碟。

innodb儲存引擎中checkpoint分為兩種:

  • sharp checkpoint:在重用redo log檔案(例如切換日誌檔案)的時候,將所有已記錄到redo log中對應的髒資料刷到磁碟。
  • fuzzy checkpoint:一次只刷一小部分的日誌到磁碟,而非將所有髒日誌刷盤。有以下幾種情況會觸發該檢查點:
    • master thread checkpoint。由master執行緒控制,每秒或每10秒刷入一定比例的髒頁到磁碟。
    • flush_lru_list checkpoint。從MySQL5.6開始可通過 innodb_page_cleaners 變數指定專門負責髒頁刷盤的page cleaner執行緒的個數,該執行緒的目的是為了保證lru列表有可用的空閒頁。
    • async/sync flush checkpoint。同步刷盤還是非同步刷盤。例如還有非常多的髒頁沒刷到磁碟(非常多是多少,有比例控制),這時候會選擇同步刷到磁碟,但這很少出現;如果髒頁不是很多,可以選擇非同步刷到磁碟,如果髒頁很少,可以暫時不刷髒頁到磁碟
    • dirty page too much checkpoint。髒頁太多時強制觸發檢查點,目的是為了保證快取有足夠的空閒空間。too much的比例由變數 innodb_max_dirty_pages_pct 控制,MySQL 5.6預設的值為75,即當髒頁佔緩衝池的百分之75後,就強制刷一部分髒頁到磁碟。

由於刷髒頁需要一定的時間來完成,所以記錄檢查點的位置是在每次刷盤結束之後才在redo log中標記的。

3.2 LSN

3.2.1 LSN概念

LSN稱為日誌的邏輯序列號,在InnoDB中佔用8個位元組

我們可以通過LSN瞭解到下面這些資訊:

  • 資料頁的版本資訊。
  • 寫入的日誌總量。
  • 檢查點的位置。

在下面兩個位置存在LSN:

  • redo log的記錄中。
  • 每個資料頁的頭部有一個變數fil_page_lsn記錄了本頁最終的LSN值是多少。

顯然,如果頁中的LSN值小於redo log中的LSN值,說明資料出現了丟失。

通過show engine innodb status可以檢視當前InnoDB的執行資訊,其中有一欄log中有關於lsn的記錄。

image-20210726150228551

  • log sequence number記錄了當前的redo log(in buffer)中的LSN。
  • log flushed up to是刷到磁碟重做日誌檔案中的LSN。
  • pages flushed up to是已經刷到磁碟資料頁上的LSN。
  • last checkpoint at是上一次檢查點所在位置的LSN。
3.2.2 LSN處理流程

(1).首先修改記憶體中的資料頁,並在資料頁中記錄LSN,暫且稱之為data_in_buffer_lsn;

(2).並且在修改資料頁的同時(幾乎是同時)向redo log in buffer中寫入redo log,並記錄下對應的LSN,暫且稱之為redo_log_in_buffer_lsn;

(3).寫完buffer中的日誌後,當觸發了日誌刷盤的幾種規則時,會向redo log file on disk刷入重做日誌,並在該檔案中記下對應的LSN,暫且稱之為redo_log_on_disk_lsn;

(4).資料頁不可能永遠只停留在記憶體中,在某些情況下,會觸發checkpoint來將記憶體中的髒頁(資料髒頁和日誌髒頁)刷到磁碟,所以會在本次checkpoint髒頁刷盤結束時,在redo log中記錄checkpoint的LSN位置,暫且稱之為checkpoint_lsn。

(5).要記錄checkpoint所在位置很快,只需簡單的設定一個標誌即可,但是刷資料頁並不一定很快,例如這一次checkpoint要刷入的資料頁非常多。也就是說要刷入所有的資料頁需要一定的時間來完成,中途刷入的每個資料頁都會記下當前頁所在的LSN,暫且稱之為data_page_on_disk_lsn。

image-20210726150846493

上圖中,從上到下的橫線分別代表:時間軸、buffer中資料頁中記錄的LSN(data_in_buffer_lsn)、磁碟中資料頁中記錄的LSN(data_page_on_disk_lsn)、buffer中重做日誌記錄的LSN(redo_log_in_buffer_lsn)、磁碟中重做日誌檔案中記錄的LSN(redo_log_on_disk_lsn)以及檢查點記錄的LSN(checkpoint_lsn)。

假設在最初時(12:0:00)所有的日誌頁和資料頁都完成了刷盤,也記錄好了檢查點的LSN,這時它們的LSN都是完全一致的。

假設此時開啟了一個事務,並立刻執行了一個update操作,執行完成後,buffer中的資料頁和redo log都記錄好了更新後的LSN值,假設為110。這時候如果執行 show engine innodb status 檢視各LSN的值,即圖中①處的位置狀態,結果會是:

log sequence number(110) > log flushed up to(100) = pages flushed up to = last checkpoint at

之後又執行了一個delete語句,LSN增長到150。等到12:00:01時,觸發redo log刷盤的規則(其中有一個規則是 innodb_flush_log_at_timeout 控制的預設日誌刷盤頻率為1秒),這時redo log file on disk中的LSN會更新到和redo log in buffer的LSN一樣,所以都等於150,這時 show engine innodb status ,即圖中②的位置,結果將會是:

log sequence number(150) = log flushed up to > pages flushed up to(100) = last checkpoint at

再之後,執行了一個update語句,快取中的LSN將增長到300,即圖中③的位置。

假設隨後檢查點出現,即圖中④的位置,正如前面所說,檢查點會觸發資料頁和日誌頁刷盤,但需要一定的時間來完成,所以在資料頁刷盤還未完成時,檢查點的LSN還是上一次檢查點的LSN,但此時磁碟上資料頁和日誌頁的LSN已經增長了,即:

log sequence number > log flushed up to 和 pages flushed up to > last checkpoint at

但是log flushed up to和pages flushed up to的大小無法確定,因為日誌刷盤可能快於資料刷盤,也可能等於,還可能是慢於。但是checkpoint機制有保護資料刷盤速度是慢於日誌刷盤的:當資料刷盤速度超過日誌刷盤時,將會暫時停止資料刷盤,等待日誌刷盤進度超過資料刷盤。

等到資料頁和日誌頁刷盤完畢,即到了位置⑤的時候,所有的LSN都等於300。

隨著時間的推移到了12:00:02,即圖中位置⑥,又觸發了日誌刷盤的規則,但此時buffer中的日誌LSN和磁碟中的日誌LSN是一致的,所以不執行日誌刷盤,即此時 show engine innodb status 時各種lsn都相等。

隨後執行了一個insert語句,假設buffer中的LSN增長到了800,即圖中位置⑦。此時各種LSN的大小和位置①時一樣。

隨後執行了提交動作,即位置⑧。預設情況下,提交動作會觸發日誌刷盤,但不會觸發資料刷盤,所以 show engine innodb status 的結果是:

log sequence number = log flushed up to > pages flushed up to = last checkpoint at

最後隨著時間的推移,檢查點再次出現,即圖中位置⑨。但是這次檢查點不會觸發日誌刷盤,因為日誌的LSN在檢查點出現之前已經同步了。假設這次資料刷盤速度極快,快到一瞬間內完成而無法捕捉到狀態的變化,這時 show engine innodb status 的結果將是各種LSN相等。

3.3 InnoDB的恢復行為

啟動InnoDB時,一定會進行恢復操作,無論上次是因為什麼原因退出。

checkpoint表示已經完整刷到磁碟上data page上的LSN,因此恢復時僅需要恢復從checkpoint開始的日誌部分。例如,當資料庫在上一次checkpoint的LSN為10000時當機,且事務是已經提交過的狀態。啟動資料庫時會檢查磁碟中資料頁的LSN,如果資料頁的LSN小於日誌中的LSN,則會從檢查點開始恢復。

還有一種情況,在當機前正處於checkpoint的刷盤過程,且資料頁的刷盤進度超過了日誌頁的刷盤進度。這時候一當機,資料頁中記錄的LSN就會大於日誌頁中的LSN,在重啟的恢復過程中會檢查到這一情況,這時超出日誌進度的部分將不會重做,因為這本身就表示已經做過的事情,無需再重做。

另外,事務日誌具有冪等性,所以多次操作得到同一結果的行為在日誌中只記錄一次。而二進位制日誌不具有冪等性,多次操作會全部記錄下來,在恢復的時候會多次執行二進位制日誌中的記錄,速度就慢得多。例如,某記錄中id初始值為2,通過update將值設定為了3,後來又設定成了2,在事務日誌中記錄的將是無變化的頁,根本無需恢復;而二進位制會記錄下兩次update操作,恢復時也將執行這兩次update操作,速度比事務日誌恢復更慢。