MySQL中的redo log和undo log
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存在的意義是確保資料庫事務的原子性。
原子性是指事務是一個不可分割的工作單位,事務中的操作要麼都發生,要麼都不發生。
-
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。
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操作,速度比事務日誌恢復更慢。