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可以分為以下兩部分

存儲在內存中的重做日志緩沖區存儲在磁盤上的重做日志文件

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 一個重做全過程的示例

以更新事務為例。

  • 將原始數據讀入內存,修改數據的內存副本。
  • 生成redo log並寫入重做日志緩沖區,redo log中存儲的是修改後的新值。
  • 事務提交時,將重做日志緩沖區中的內容刷新到重做日志文件。
  • 隨後正常將內存中的臟頁刷回磁盤。

1.6 持久性的保證

1.6.1 Force Log at Commit機制

Force Log at Commit機制實現瞭事務的持久性。在內存中操作時,日志被寫入重做日志緩沖區。但在事務提交之前,必須首先將所有日志寫入磁盤上的重做日志文件。

為瞭確保每個日志都寫入重做日志文件,必須使用一個fsync系統調用,確保OS buffer中的日志被完整地寫入磁盤上的log file。

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

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,所以相比第二種情況,性能會差一些,但還是比第一種好的。

無論是哪種情況

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保證在返回之前將記錄刷新到磁盤,最大化瞭數據的安全性。

第二個級別是write-only,保證記錄寫入操作系統。這允許數據在進程級別的崩潰後幸存。

最不嚴格的是no-sync,將記錄保存在內存緩沖區中,不保證立即寫入文件系統。

強制日志再提交。即Force-log-at-commit,它要求提交事務時必須把所有迷你事務日志刷新到磁盤。

1.7 寫redo log的過程

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

  • 每個數據修改後被寫入迷你事務私有緩沖區。
  • 當更新語句完成,redo log從迷你事務私有緩沖區被寫入內存中的公共日志緩沖區。
  • 提交外部事務時,會將重做日志緩沖區刷入重做日志文件。

1.8 日志塊 log block

redo log以塊為單位進行存儲,每個塊大小為512字節。無論是在內存重做日志緩沖區、操作系統緩沖區還是重做日志文件中,都是以這樣的512字節大小的塊進行存儲的。

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

  • 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下。

2 撤銷日志undo log

2.1 關於undo log

undo log存在的意義是確保數據庫事務的原子性。

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

  • edo 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也要實現持久性保護。

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。

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進行連接。

在執行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的記錄。

  • 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。

上圖中,從上到下的橫線分別代表:時間軸、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操作,速度比事務日志恢復更慢。

到此這篇關於MySQL中的redo log和undo log的文章就介紹到這瞭,更多相關MySQL中的redo log和undo log內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: