天天看點

MySQL 事務日志詳解

作者:IT智能化專欄

目錄

1.redo 日志

1.1.為什麼需要 REDO 日志?

1.2.REDO 日志的好處與特點

1.3.redo 的組成

1.4.redo 的整體流程

1.5.redo log 的刷盤政策

1.6.不同刷盤政策示範

1.6.1.流程圖說明

1.6.2.舉例

1.7.寫入 redo log buffer 過程

1.7.1.補充概念:Mini-Transaction

1.7.2.redo 日志寫入 log buffer

1.7.3.redo log block 的結構圖

1.8.redo log file

1.8.1.相關參數設定

1.8.2.日志檔案組

1.8.3.checkpoint

1.9.redo 小結

2.Undo 日志

2.1.如何了解 undo 日志?

2.2.undo 日志的作用

2.3.undo 的存儲結構

2.3.1.復原段與 undo 頁

2.3.2.復原段與事務

2.3.3.復原段中的資料分類

2.4.undo 的類型

2.5.undo log 的生命周期

2.5.1.簡要生成過程

2.5.2.詳細生成過程

2.5.3.undo log 是如何復原的

2.5.4.undo log 的删除

2.6.undo 小結

本文筆記整理來自尚矽谷視訊https://www.bilibili.com/video/BV1iq4y1u7vj?p=169,相關資料可在視訊評論區進行擷取。

(1)事務有 4 種特性:原子性、一緻性、隔離性和持久性。那麼事務的四種特性到底是基于什麼機制實作呢?

  • 事務的隔離性由鎖機制實作。
  • 而事務的原子性、一緻性和持久性由事務的 redo 日志和 undo 日志來保證。REDO LOG 稱為重做日志,提供再寫入操作,恢複送出事務修改的頁操作,用來保證事務的持久性。UNDO LOG 稱為復原日志,復原行記錄到某個特定版本,用來保證事務的原子性、一緻性。

(2)有的 DBA 或許會認為 UNDO 是 REDO 的逆過程,其實不然。REDO 和 UNDO 都可以視為是一種恢複操作,但是:

  • redo log:是存儲引擎層 (innodb) 生成的日志,記錄的是實體級别上的頁修改操作,比如頁号xxx、偏移量 yyy 寫入了 ‘zzz’ 資料。主要為了保證資料的可靠性;
  • undo log:是存儲引擎層 (innodb) 生成的日志,記錄的是邏輯記錄檔,比如對某一行資料進行了 INSERT 語句操作,那麼 undo log 就記錄一條與之相反的 DELETE 操作。主要用于事務的復原(undo log 記錄的是每個修改操作的逆操作)和一緻性非鎖定讀(undo log 復原行記錄到某種特定的版本—MVCC,即多版本并發控制)。

1.redo 日志

InnoDB 存儲引擎是以頁為機關來管理存儲空間的。在真正通路頁面之前,需要把在磁盤上的頁緩存到記憶體中的 Buffer Pool 之後才可以通路。所有的變更都必須先更新緩沖池中的資料,然後緩沖池中的髒頁會以一定的頻率被刷入磁盤(checkPoint 機制),通過緩沖池來優化 CPU 和磁盤之間的鴻溝,這樣就可以保證整體的性能不會下降太快。

1.1.為什麼需要 REDO 日志?

(1)一方面,緩沖池可以幫助我們消除 CPU 和磁盤之間的鴻溝,checkpoint 機制可以保證資料的最終落盤,然而由于 checkpoint 并不是每次變更的時候就觸發的,而是 master 線程隔一段時間去處理的。是以最壞的情況就是事務送出後,剛寫完緩沖池,資料庫當機了,那麼這段資料就是丢失的,無法恢複。

(2)另一方面,事務包含持久性的特性,就是說對于一個已經送出的事務,在事務送出後即使系統發生了崩潰,這個事務對資料庫中所做的更改也不能丢失。那麼如何保證這個持久性呢? 一個簡單的做法:在事務送出完成之前把該事務所修改的所有頁面都重新整理到磁盤,但是這個簡單粗暴的做法有些問題:

① 修改量與重新整理磁盤工作量嚴重不成比例

有時候我們僅僅修改了某個頁面中的一個位元組,但是我們知道在 InnoDB 中是以頁為機關來進行磁盤 I/O 的,也就是說我們在該事務送出時不得不将一個完整的頁面從記憶體中重新整理到磁盤,我們又知道一個頁面預設是 16KB 大小,隻修改一個位元組就要重新整理 16KB 的資料到磁盤上顯然是太小題大做了。

② 随機 I/O 重新整理較慢

一個事務可能包含很多語句,即使是一條語句也可能修改許多頁面,假如該事務修改的這些頁面可能并不相鄰,這就意味着在将某個事務修改的 Buffer Pool 中的頁面重新整理到磁盤時,需要進行很多的随機 I/O,随機 I/O 比順序 I/O 要慢,尤其對于傳統的機械硬碟來說。

(3)另一個解決的思路 :我們隻是想讓已經送出了的事務對資料庫中資料所做的修改永久生效,即使後來系統崩潰,在重新開機後也能把這種修改恢複出來。是以我們其實沒有必要在每次事務送出時就把該事務在記憶體中修改過的全部頁面重新整理到磁盤,隻需要把修改了哪些東西記錄一下就好。比如,某個事務将系統表空間中第 10 号頁面中偏移量為 100 處的那個位元組的值 1 改成 2 。我們隻需要記錄一下:将第 0 号表空間的 10 号頁面的偏移量為 100 處的值更新為 2 。

(4)InnoDB引擎的事務采用了 WAL 技術 (Write-Ahead Logging ),這種技術的思想就是先寫日志,再寫磁盤,隻有日志寫入成功,才算事務送出成功,這裡的日志就是 redo log。當發生當機且資料未刷到磁盤的時候,可以通過 redo log 來恢複,保證 ACID 中的 D,這就是 redo log 的作用。

MySQL 事務日志詳解

1.2.REDO 日志的好處與特點

(1)好處

① redo 日志降低了刷盤頻率;

② redo 日志占用的空間非常小;

存儲表空間 ID、頁号、偏移量以及需要更新的值,所需的存儲空間是很小的,刷盤快。

(2)特點

① redo 日志是順序寫入磁盤的:在執行事務的過程中,每執行一條語句,就可能産生若幹條 redo 日志,這些日志是按照産生的順序寫入磁盤的,也就是使用順序 l/O,效率比随機 I/O 快。

② 事務執行過程中,redo log 不斷記錄:redo log 跟 bin log 的差別,redo log 是存儲引擎層産生的,而 bin log 是資料庫層産生的。假設一個事務,對表做 10 萬行的記錄插入,在這個過程中,一直不斷的往 redo log 順序記錄,而 bin log 不會記錄,直到這個事務送出,才會—次寫入到 bin log 檔案中。

1.3.redo 的組成

Redo log 可以簡單分為以下兩個部分:

(1)重做日志緩沖 (redo log buffer),儲存在記憶體中,是易失的。

在伺服器啟動時就向作業系統申請了一大片稱之為 redo log buffer 的連續記憶體空間,翻譯成中文就是 redo 日志緩沖區。這片記憶體空間被劃分成若幹個連續的 redo log block。一個 redo log block 占用 512 位元組大小。

MySQL 事務日志詳解

參數設定:innodb_log_buffer_size,它表示 redo log buffer 大小,預設是 16M ,最大值是 4096M,最小值為 1M。

mysql> show variables like '%innodb_log_buffer_size%'; 
+------------------------+----------+
| Variable_name 		 | Value 	| 
+------------------------+----------+
| innodb_log_buffer_size | 16777216 | 
+------------------------+----------+
123456           

(2)重做日志檔案 (redo log file),儲存在硬碟中,是持久的。

redo 日志檔案如圖所示,其中的 ib_logfile0 和 ib_logfile1 即為 redo 日志。

cd /var/lib/mysql
ll
12           
MySQL 事務日志詳解

1.4.redo 的整體流程

(1)以一個更新事務為例,redo log 流轉過程,如下圖所示:

MySQL 事務日志詳解

① 第 1 步:先将原始資料從磁盤中讀入記憶體中來,修改資料的記憶體拷貝;

② 第 2 步:生成一條重做日志并寫入 redo log buffer,記錄的是資料被修改後的值;

③ 第 3 步:當事務 commit 時,将 redo log buffer 中的内容重新整理到 redo log file,對 redo log file采用追加寫的方式;

④ 第 4 步:定期将記憶體中修改的資料重新整理到磁盤中;

(2)Write-Ahead Log(預先日志持久化):在持久化一個資料頁之前,先将記憶體中相應的日志頁持久化。

1.5.redo log 的刷盤政策

(1)redo log 的寫入并不是直接寫入磁盤的,InnoDB 引擎會在寫 redo log 的時候先寫 redo log buffer,之後以一定的頻率刷入到真正的 redo log file 中(即上面的第 3 步)。這裡的一定頻率怎麼看待呢?這就是我們要說的刷盤政策。

MySQL 事務日志詳解

(2)注意,redo log buffer 刷盤到 redo log file 的過程并不是真正的刷到磁盤中去,隻是刷入到檔案系統緩存 (page cache) 中去(這是現代作業系統為了提高檔案寫入效率做的一個優化),真正的寫入會交給系統自己來決定(比如 page cache 足夠大了)。那麼對于 InnoDB 來說就存在一個問題,如果交給系統來同步,同樣如果系統當機,那麼資料也丢失了(雖然整個系統當機的機率還是比較小的)。

(3)針對這種情況,InnoDB 給出 innodb_flush_log_at_trx_commit 參數,該參數控制 commit 送出事務時,如何将 redo log buffer 中的日志重新整理到 redo log file 中。它支援以下三種政策:

① 設定為0 :表示每次事務送出時不進行刷盤操作(系統預設 master thread 每隔 1s 進行一次重做日志的同步);

② 設定為1 :表示每次事務送出時都将進行同步,刷盤操作(預設值);

③ 設定為2 :表示每次事務送出時都隻把 redo log buffer 内容寫入 page cache,不進行同步。由作業系統自己決定什麼時候同步到磁盤檔案;

mysql> show variables like 'innodb_flush_log_at_trx_commit';
+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1     |
+--------------------------------+-------+
1 row in set (0.01 sec)
1234567           

(4)另外,InnoDB 存儲引擎有一個背景線程,每隔 1s,就會把 redo log buffer 中的内容寫到檔案系統緩存,然後調用刷盤操作。

MySQL 事務日志詳解

也就是說,一個沒有送出事務的 redo log 記錄,也可能會刷盤。因為在事務執行過程 redo log 記錄是會寫入 redo log buffer 中,這些 redo log 記錄會被背景線程刷盤。

MySQL 事務日志詳解

除了背景線程每秒 1 次的輪詢操作,還有一種情況,當 redo log buffer 占用的空間即将達到 innodb_log_buffer_size(這個參數預設是 16M)的一半的時候,背景線程會主動刷盤。

1.6.不同刷盤政策示範

1.6.1.流程圖說明

(1)innodb_flush_log_at_trx_commit = 1

MySQL 事務日志詳解
小結:innodb_flush_log_at_trx_commit = 1 時,隻要事務送出成功,redo log 記錄就一定在硬碟裡,不會有任何資料丢失。如果事務執行期間 MySQL 挂了或當機,這部分日志丢了,但是事務并沒有送出,是以日志丢了也不會有損失。可以保證 ACID 的 D,資料絕對不會丢失,但是效率是最差的。建議使用預設值,雖然作業系統當機的機率理論小于資料庫當機的機率,但是一般既然使用了事務,那麼資料的安全相對來說更重要些。

(2)innodb_flush_log_at_trx_commit = 2

MySQL 事務日志詳解
小結:innodb_flush_log_at_trx_commit = 2 時,隻要事務送出成功,redo log buffer中的内容隻寫入檔案系統緩存 (page cache)。如果僅僅隻是 MySQL 挂了不會有任何資料丢失,但是作業系統當機可能會有 1s 資料的丢失,這種情況下無法滿足 ACID 中的 D。但是數值 2 肯定是效率最高的。

(3)innodb_flush_log_at_trx_commit = 0

MySQL 事務日志詳解
小結:innodb_flush_log_at_trx_commit = 0 時,master thread 中每 1s 進行一次重做日志的 fsync 操作,是以執行個體 crash 最多丢失 1s 内的事務(master thread 是負責将緩沖池中的資料異步重新整理到磁盤,保證資料的一緻性)。值為 0 的話,是一種折中的做法,它的 I/O 效率理論是高于 1 的,低于 2 的,這種政策也有丢失資料的風險,也無法保證 ACID 中的 D。

1.6.2.舉例

(1)建立表和存儲過程

USE atguigudb3;

CREATE TABLE test_load(
a INT,
b CHAR(80)
)ENGINE=INNODB;

# 建立存儲過程,用于向 test_load 中添加資料
DELIMITER//
CREATE PROCEDURE p_load(COUNT INT UNSIGNED)
BEGIN
DECLARE s INT UNSIGNED DEFAULT 1;
DECLARE c CHAR(80)DEFAULT REPEAT('a',80);
WHILE s<=COUNT DO
INSERT INTO test_load SELECT NULL,c;
COMMIT;
SET s=s+1;
END WHILE;
END //
DELIMITER;
1234567891011121314151617181920           

(2)測試 1:innodb_flush_log_at_trx_commit = 1(預設情況)

# 調用存儲過程,并檢視所用的時間,如下圖所示
CALL p_load(30000);  # 耗時 19.427s
12           
MySQL 事務日志詳解

(3)測試 2:innodb_flush_log_at_trx_commit = 0

# 清空表資料,防止已有資料幹擾本次測試
TRUNCATE TABLE test_load;

SELECT COUNT(*) FROM test_load;

SET GLOBAL innodb_flush_log_at_trx_commit = 0;

SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';

# 調用存儲過程
CALL p_load(30000); # 耗時 0.447 sec
1234567891011           
MySQL 事務日志詳解

(3)測試 2:innodb_flush_log_at_trx_commit = 2

TRUNCATE TABLE test_load;

SELECT COUNT(*) FROM test_load;

SET GLOBAL innodb_flush_log_at_trx_commit = 2;

SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';

# 調用存儲過程
CALL p_load(30000); # 耗時 3.848 sec
12345678910           
MySQL 事務日志詳解

1.7.寫入 redo log buffer 過程

1.7.1.補充概念:Mini-Transaction

(1)MySQL 把對底層頁面中的一次原子通路的過程稱之為一個 Mini-Transaction,簡稱 mtr,比如,向某個索引對應的 B+ 樹中插入一條記錄的過程就是一個 Mini-Transaction。一個 mtr 可以包含一組 redo 日志,在進行崩潰恢複時這一組 redo 日志作為一個不可分割的整體。

(2)一個事務可以包含若幹條語句,每一條語句其實是由若幹個 mtr 組成,每一個 mtr 又可以包含若幹條 redo 日志,畫個圖表示它們的關系就是這樣:

MySQL 事務日志詳解

1.7.2.redo 日志寫入 log buffer

(1)向 log buffer 中寫入 redo 日志的過程是順序的,也就是先往前邊的 block 中寫,當該 block 的空閑空間用完之後再往下一個 block 中寫。當我們想往 log buffer 中寫入 redo 日志時,第一個遇到的問題就是應該寫在哪個 block 的哪個偏移量處,是以 InnoDB 的設計者特意提供了一個稱之為 buf_free 的全局變量,該變量指明後續寫入的 redo 日志應該寫入到 log buffer 中的哪個位置,如圖所示:

MySQL 事務日志詳解

(2)一個 mtr 執行過程中可能産生若幹條 redo 日志,這些 redo 日志是一個不可分割的組,是以其實并不是每生成一條 redo 日志,就将其插入到log buffer 中,而是每個 mtr 運作過程中産生的日志先暫時存到一個地方,當該 mtr 結束的時候,将過程中産生的一組 redo 日志再全部複制到 log buffer 中。我們現在假設有兩個名為 T1、T2 的事務,每個事務都包含 2 個 mtr,我們給這幾個 mtr 命名一下:

  • 事務 T1 的兩個 mtr 分别稱為 mtr_T1_1 和 mtr_T1_2。
  • 事務 T2 的兩個 mtr 分别稱為 mtr_T2_1 和 mtr_T2_2。

(3)每個 mtr 都會産生一組 redo 日志,用示意圖來描述一下這些 mtr 産生的日志情況:

MySQL 事務日志詳解

(4)不同的事務可能是并發執行的,是以 T1、T2 之間的 mtr 可能是交替執行的。每當一個 mtr 執行完成時,伴随該 mtr 生成的一組 redo 日志就需要被複制到 log buffer 中,也就是說不同僚務的 mtr 可能是交替寫入 log buffer 的,我們畫個示意圖(為了美觀,我們把一個 mtr 中産生的所有的 redo 日志當作一個整體來畫):

MySQL 事務日志詳解

有的 mtr 産生的 redo 日志量非常大,比如 mtr_t1_2 産生的 redo 日志占用空間比較大,占用了 3 個 block 來存儲。

1.7.3.redo log block 的結構圖

(1)一個 redo log block 是由日志頭、日志體、日志尾組成。日志頭占用 12 位元組,日志尾占用 8 位元組,是以一個 block 真正能存儲的資料就是 512 - 12 - 8 = 492 位元組。

為什麼一個 block 設計成 512 位元組?

這個和磁盤的扇區有關,機械磁盤預設的扇區就是 512 位元組,如果你要寫入的資料大于 512 位元組,那麼要寫入的扇區肯定不止一個,這時就要涉及到盤片的轉動,找到下一個扇區,假設現在需要寫入兩個扇區 A 和 B,如果扇區 A 寫入成功,而扇區 B 寫入失敗,那麼就會出現非原子性的寫入,而如果每次隻寫入和扇區的大小一樣的 512 位元組,那麼每次的寫入都是原子性的。

MySQL 事務日志詳解

(2)真正的 redo 日志都是存儲到占用 496 位元組大小的 log block body 中,圖中的 log block header 和 logblock trailer 存儲的是一些管理資訊。我們來看看這些所謂的管理資訊都有什麼。

MySQL 事務日志詳解

① log block header 的屬分别如下:

LOG_BLOCK_HDR_NO log buffer 是由 log block 組成,在内部 log buffer 就好似一個數組,是以 LOG_BLOCK_HDR_NO 用來标記這個數組中的位置。其是遞增并且循環使用的,占用 4 個位元組,但是由于第一位用來判斷是否是 flush bit,是以最大的值為 2G
LOG_BLOCK_HDR_DATA_LEN 表示 block 中已經使用了多少位元組,初始值為 12(因為 log block body 從第 12 個位元組處開始)。随着往 block 中寫入的 redo 日志越來也多,本屬性值也跟着增長。如果 log block body 已經被全部寫滿,那麼本屬性的值被設定為 512
LOG_BLOCK_FIRST_REC_GROUP 一條 redo 日志也可以稱之為一條 redo 日志記錄 (redo log record),一個 mtr 會生産多條 redo 日志記錄,這些 redo 日志記錄被稱之為一個 redo 日志記錄組(redo log record group)。LOG_BLOCK_FIRST_REC_GROUP 就代表該 block 中第一個 mtr 生成的 redo 日志記錄組的偏移量(其實也就是這個 block 裡第一個 mtr 生成的第一條 redo 日志的偏移量)。如果該值的大小和LOG_BLOCK_HDR_DATA_LEN 相同,則表示目前 log block 不包含新的日志
LOG_BLOCK_CHECKPOINT_NO 占用 4 位元組,表示該 log block 最後被寫入時的 checkpoint

② log block trailer 中屬性如下:

LOG_BLOCK_CHECKSUM:表示 block 的校驗值,用于正确性校驗(其值和 LOG_BLOCK_HDR_NO 相同),我們暫時不關心它。

1.8.redo log file

1.8.1.相關參數設定

(1)innodb_log_group_home_dir:指定 redo log 檔案組所在的路徑,預設值為 ./,表示在資料庫的資料目錄下。MySQL 的預設資料目錄 (var/lib/mysql) 下預設有兩個名為 ib_logfile0 和 ib_logfile1 的檔案,log buffer 中的日志預設情況下就是重新整理到這兩個磁盤檔案中。此 redo 日志檔案位置還可以修改。

(2)innodb_log_files_in_group:指明 redo log file 的個數,命名方式如:ib_logfile0,iblogfile1… iblogfilen。預設 2 個,最大 100 個。

mysql> show variables like 'innodb_log_files_in_group'; 
+---------------------------+-------+
| Variable_name 	 	    | Value | 
+---------------------------+-------+
| innodb_log_files_in_group | 2 	| 
+---------------------------+-------+
# ib_logfile0 
# ib_logfile1
12345678           

(3)innodb_flush_log_at_trx_commit:控制 redo log 重新整理到磁盤的政策,預設為 1。

(4)innodb_log_file_size:單個 redo log 檔案設定大小,預設值為 48M 。最大值為 512G,注意最大值

指的是整個 redo log 系列檔案之和,即 innodb_log_files_in_group * innodb_log_file_size 不能大于最大值 512G。

mysql> show variables like 'innodb_log_file_size'; 
+----------------------+----------+
| Variable_name 	   | Value	  | 
+----------------------+----------+
| innodb_log_file_size | 50331648 | 
+----------------------+----------+
123456           

根據業務修改其大小,以便容納較大的事務。編輯 my.cnf文 件并重新開機資料庫生效,如下所示:

[root@localhost ~]vim /etc/my.cnf 
innodb_log_file_size=200M
12           
在資料庫執行個體更新比較頻繁的情況下,可以适當加大 redo log 組數和大小。但也不推薦 redo log 設定過大,在 MySQL 崩潰恢複時會重新執行 REDO 日志中的記錄。

1.8.2.日志檔案組

(1)從上邊的描述中可以看到,磁盤上的 redo 日志檔案不隻一個,而是以一個日志檔案組的形式出現的。這些檔案以 ib_logfile[數字](數字可以是0、1、2…)的形式進行命名,每個的 redo 日志檔案大小都是一樣的。

(2)在将 redo 日志寫入日志檔案組時,是從 ib_logfile0 開始寫,如果 ib_logfile0 寫滿了,就接着 ib_logfile1 寫。同理, ib_logfile1 寫滿了就去寫 ib_logfile2,依此類推。如果寫到最後一個檔案該咋辦?那就重新轉到 ib_logfile0 繼續寫,是以整個過程如下圖所示:

MySQL 事務日志詳解

(3)總共的 redo 日志檔案大小其實就是: innodb_log_file_size × innodb_log_files_in_group。采用循環使用的方式向 redo 日志檔案組裡寫資料的話,會導緻後寫入的 redo 日志覆寫掉前邊寫的 redo 日志?當然!是以 InnoDB 的設計者提出了 checkpoint 的概念。

1.8.3.checkpoint

(1)在整個日志檔案組中還有兩個重要的屬性,分别是 write pos、checkpoint:

  • write pos是目前記錄的位置,一邊寫一邊後移;
  • checkpoint是目前要擦除的位置,也是往後推移;

(2)每次刷盤 redo log 記錄到日志檔案組中,write pos 位置就會後移更新。每次 MySQL 加載日志檔案組恢複資料時,會清空加載過的redo log 記錄,并把 checkpoint 後移更新。write pos 和 checkpoint 之間的還空着的部分可以用來寫入新的 redo log 記錄。

MySQL 事務日志詳解

(3)如果 write pos 追上checkpoint,表示日志檔案組滿了,這時候不能再寫入新的 redo log 記錄,MySQL 得停下來,清空一些記錄,把 checkpoint 推進一下。

MySQL 事務日志詳解

1.9.redo 小結

相信大家都知道 redo log 的作用和它的刷盤時機、存儲形式:InnoDB 的更新操作采用的是 Write Ahead Log(預先日志持久化)政策,即先寫日志,再寫入磁盤。

MySQL 事務日志詳解

2.Undo 日志

redo log 是事務持久性的保證,undo log 是事務原子性的保證。在事務中更新資料的前置操作其實是要先寫入一個 undo log。

2.1.如何了解 undo 日志?

(1)事務需要保證原子性,也就是事務中的操作要麼全部完成,要麼什麼也不做。但有時候事務執行到一半會出現一些情況,比如:

  • 情況一:事務執行過程中可能遇到各種錯誤,比如伺服器本身的錯誤, 作業系統錯誤 ,甚至是突然斷電導緻的錯誤。
  • 情況二:程式員可以在事務執行過程中手動輸入 ROLLBACK 語句結束目前事務的執行。

(2)以上情況出現,我們需要把資料改回原先的樣子,這個過程稱之為復原,這樣就可以造成一個假象:這個事務看起來什麼都沒做,是以符合原子性要求。

(3)每當我們要對一條記錄做改動時(這裡的改動可以指 INSERT、DELETE、UPDATE,不包括 SELECT),都需要"留一手"——即把復原時所需的東西記下來。比如:

  • 你插入一條記錄時,至少要把這條記錄的主鍵值記下來,之後復原的時候隻需要把這個主鍵值對應的記錄融掉就好了。(對于每個INSERT,InnoDB 存儲引擎會完成一個DELETE)
  • 你删除了一·條記錄,至少要把這條記錄中的内容都記下來,這樣之後復原時再把由這些内容組成的記錄插入到表中就好了。(對于每個DELETE,InnoDB 存儲引擎會執行一個 INSERT)
  • 你修改了一荼記綠,至少要把修改這條記錄前的舊值都記錄下來,這樣之後復原時再把這條記錄更新為舊值就好了。(對于每個UPDATE,InnoDB 存儲引擎會執行一個相反的 UPDATE,将修改前的行放回去)

(4)MySQL 把這畢為了復原而記錄的這些内容稱之為撤銷日志或者復原日志(即 undo log)。注意,由于查詢操作 (SELECT) 并不會修改任何使用者記錄,是以在查詢操作執行時,并不需要記錄相應的 undo 日志。此外,undo log 會産生 redo log,也就是 undo log 的産生會伴随着 redo log 的産生,這是因為 undo log 也需要持久性的保護。

2.2.undo 日志的作用

(1)作用 1:復原資料

① 使用者對 undo 日志可能有誤解:undo 用于将資料庫實體地恢複到執行語句或事務之前的樣子。但事實并非如此。undo 是邏輯日志,是以隻是将資料庫邏輯地恢複到原來的樣子。所有修改都被邏輯地取消了,但是資料結構和頁本身在復原之後可能大不相同。

② 這是因為在多使用者并發系統中,可能會有數十、數百甚至數千個并發事務。資料庫的主要任務就是協調對資料記錄的并發通路。比如,一個事務在修改目前一個頁中某幾條記錄,同時還有别的事務在對同一個頁中另幾條記錄進行修改。是以,不能将一個頁復原到事務開始的樣子,因為這樣會影響其他事務正在進行的工作。

(2)作用 2:MVCC

undo 的另一個作用是 MVCC(多版本并發控制),即在 InnoDB 存儲引擎中 MVCC 的實作是通過 undo 來完成。當使用者讀取一行記錄時,若該記錄已經被其他事務占用,目前事務可以通過 undo 讀取之前的行版本資訊,以此實作非鎖定讀取。

2.3.undo 的存儲結構

2.3.1.復原段與 undo 頁

(1)InnoDB 對 undo log 的管理采用段的方式,也就是復原段 (rollback segment)。每個復原段記錄了 1024 個 undo log segment,而在每個undo log segment 段中進行 undo 頁的申請。

  • 在 InnoDB1.1版本之前(不包括1.1版本),隻有一個 rollback segment,是以支援同時線上的事務限制為 1024 。雖然對絕大多數的應用來說都已經夠用。
  • 從 1.1 版本開始 InnoDB 支援最大 128 個 rollback segment ,故其支援同時線上的事務限制提高到了 128*1024 。
mysql> show variables like 'innodb_undo_logs'; 
+------------------+-------+
| Variable_name    | Value | 
+------------------+-------+
| innodb_undo_logs | 128   | 
+------------------+-------+
123456           

(2)雖然 InnoDB 1.1 版本支援了 128 個 rollback segment,但是這些 rollback segment 都存儲于共享表空間 ibdata 中。從 InnoDB1.2 版本開始,可通過參數對 rollback segment 做進一步的設定。這些參數包括:

  • innodb_undo_directory:設定 rollback segment 檔案所在的路徑。這意味着 rollback segment 可以存放在共享表空間以外的位置,即可以設定為獨立表空間。該參數的預設值為 “./”,表示目前 InnoDB 存儲引擎的目錄。
  • innodb_undo_logs:設定 rollback segment 的個數,預設值為 128。在 InnoDB 1.2 版本中,該參數用來替換之前版本的參數innodb_rollback_segments。
  • innodb_undo_tablespaces:設定構成 rollback segment 檔案的數量,這樣 rollback segment 可以較為平均地分布在多個檔案中。設定該參數後,會在路徑 innodb_undo_directory 看到 undo 為字首的檔案,該檔案就代表 rollback segment 檔案。
注意:undo log 相關參數一般很少改動。

(3)undo 頁的重用

① 當我們開啟一個事務需要寫 undo log 的時候,就得先去 undo log segment 中去找到一個空閑的位置,當有空位的時候,就去申請 undo頁,在這個申請到的 undo 頁中進行 undo log 的寫入。我們知道 MySQL 預設一頁的大小是 16k。

② 為每一個事務配置設定一個頁,是非常浪費的(除非你的事務非常長),假設你的應用的 TPS(每秒處理的事務數目)為1000,那麼 1s 就需要 1000 個頁,大概需要 16M 的存儲,1 分鐘大概需要 1G 的存儲。如果照這樣下去除非 MySQL 清理的非常勤快,否則随着時間的推移,磁盤空間會增長的非常快,而且很多空間都是浪費的。

③ 于是 undo 頁就被設計的可以重用了,當事務送出時,并不會立刻删除 undo 頁。因為重用,是以這個 undo 頁可能混雜着其他事務的undo log。undo log 在 commit 後,會被放到一個連結清單中,然後判斷 undo 頁的使用空間是否小于 3/4,如果小于 3/4 的話,則表示目前的undo 頁可以被重用,那麼它就不會被回收,其他事務的 undo log 可以記錄在目前 undo 頁的後面。由于 undo log 是離散的,是以清理對應的磁盤空間時,效率不高。

2.3.2.復原段與事務

(1)每個事務隻會使用一個復原段,一個復原段在同一時刻可能會服務于多個事務。

(2)當一個事務開始的時候,會制定一個復原段,在事務進行的過程中,當資料被修改時,原始的資料會被複制到復原段。

(3)在復原段中,事務會不斷填充盤區,直到事務結束或所有的空間被用完。如果目前的盤區不夠用,事務會在段中請求擴充下一個盤區,如果所有已配置設定的盤區都被用完,事務會覆寫最初的盤區或者在復原段允許的情況下擴充新的盤區來使用。

(4)復原段存在于undo表空間中,在資料庫中可以存在多個 undo 表空間,但同一時刻隻能使用一個 undo 表空間。

mysql> show variables like 'innodb_undo_tablespaces';
+-------------------------+-------+
| Variable_name    		  | Value | 
+-------------------------+-------+
| innodb_undo_tablespaces |   2   | 
+-------------------------+-------+
# undo log 的數量最少為 2,undo log 的 truncate 操作有 purge 協調線程發起。
# 在 truncate 某個 undo log 表空間的過程中,保證有一個可用的 undo log 可用。
12345678           

(5)當事務送出時,InnoDB 存儲引擎會做以下兩件事情:

① 将 undo log 放入清單中,以供之後的 purge 操作

② 判斷 undo log 所在的頁是否可以重用,若可以配置設定給下個事務使用

2.3.3.復原段中的資料分類

(1)未送出的復原資料 (uncommitted undo information)

該資料所關聯的事務并未送出,用于實作讀一緻性,是以該資料不能被其他事務的資料覆寫。

(2)已經送出但未過期的復原資料 (committed undo information)

該資料關聯的事務已經送出,但是仍受到 undo retention 參數的保持時間的影響。

(3)事務已經送出并過期的資料 (expired undo information)

事務已經送出,而且資料儲存時間已經超過 undo retention 參數指定的時間,屬于已經過期的資料。當復原段滿了之後,會優先覆寫"事務已經送出并過期的資料"。

事務送出後并不能馬上删除 undo log 及 undo log 所在的頁。這是因為可能還有其他事務需要通過 undo log 來得到行記錄之前的版本。是以事務送出時将 undo log 放入一個連結清單中,是否可以最終删除 undo log 及 undo log 所在頁由 purge 線程來判斷。

2.4.undo 的類型

在 InnoDB 存儲引擎中,undo log 分為:

  • insert undo log

    insert undo log 是指在 insert 操作中産生的 undo log。因為 insert 操作的記錄,隻對事務本身可見,對其他事務不可見(這是事務隔離性的要求),故該 undo log 可以在事務送出後直接删除。不需要進行 purge 操作。

  • update undo log

    update undo log 記錄的是對 delete 和 update 操作産生的 undo log。該 undo log 可能需要提供 MVCC 機制,是以不能在事務送出時就進行删除。送出時放入 undo log 連結清單,等待 purge 線程進行最後的删除。

2.5.undo log 的生命周期

2.5.1.簡要生成過程

(1)以下是 undo + redo 事務的簡化過程,假設有 2 個數值,分别為 A = 1 和 B = 2,然後将 A 修改為 ,B 修改為 4。

1. start transaction;
2. 記錄 A = 1 到 undo log;
3. update A = 3;
4. 記錄 A = 3 到 redo log;
5. 記錄 B = 2 到 undo log;
6. update B =4;
7. 記錄 B = 4 到 redo log;
8. 将 redo log 重新整理到磁盤;
9. commit;
123456789           

在 1 ~ 8 步驟的任意一步系統當機,事務未送出,該事務就不會對磁盤上的資料做任何影響。

如果在 8 ~ 9 之間當機,恢複之後可以選擇復原,也可以選擇繼續完成事務送出

因為此時 redo log 已經持久化。若在 9 之後系統當機,記憶體映射中變更的資料還來不及刷回磁盤,那麼系統恢複之後,可以根據 redo log 把資料刷回磁盤。

(2)隻有 Buffer Pool 的流程

MySQL 事務日志詳解

(3)有了Redo Log和Undo Log之後

MySQL 事務日志詳解

在更新 Buffer Pool 中的資料之前,我們需要先将該資料事務開始之前的狀态寫入 Undo Log 中。假設更新到一半出錯了,我們就可以通過 Undo Log 來復原到事務開始前。

2.5.2.詳細生成過程

(1)對于 InnoDB 引擎來說,每個行記錄除了記錄本身的資料之外,還有幾個隐藏的列:

① DB_ROW_ID∶如果沒有為表顯式地定義主鍵,并且表中也沒有定義唯一索引,那麼 InnoDB 會自動為表添加一個 row_id 的隐藏列作為主鍵。

② DB_TRX_ID:每個事務都會配置設定一個事務 ID,當對某條記錄發生變更時,就會将這個事務的事務 ID 寫入 trx_id 中。

③ DB_ROLL_PTR:復原指針,本質上就是指向 undo log 的指針。

MySQL 事務日志詳解

(2)當執行 INSERT 操作時:

begin; 
INSERT INTO user (name) VALUES ("tom");
12           

插入的資料都會生成一條 insert undo log,并且資料的復原指針會指向它。undo log 會記錄 undo log 的序号、插入主鍵的列和值…,那麼在進行 rollback 的時候,通過主鍵直接把對應的資料删除即可。

MySQL 事務日志詳解

(3)當執行 UPDATE 操作時:

對于更新的操作會産生 update undo log,并且會分更新主鍵的和不更新主鍵的,假設現在執行:

UPDATE user SET name= "Sun" WHERE id = 1;
1           
MySQL 事務日志詳解

這時會把老的記錄寫入新的 undo log,讓復原指針指向新的 undo log,它的 undo no 是 1,并且新的 undo log 會指向老的 undo log (undo no = 0)。假設現在執行:

UPDATE user SET id=2 WHERE id=1;
1           
MySQL 事務日志詳解

對于更新主鍵的操作,會先把原來的資料 deletemark 辨別打開,這時并沒有真正的删除資料,真正的删除會交給清理線程去判斷,然後在後面插入一條新的資料,新的資料也會産生 undo log,并且 undo log 的序号會遞增。

可以發現每次對資料的變更都會産生一個 undo log,當一條記錄被變更多次時,那麼就會産生多條 undo log,undo log 記錄的是變更前的日志,并且每個 undo log 的序号是遞增的,那麼當要復原的時候,按照序号依次向前推,就可以找到我們的原始資料了。

2.5.3.undo log 是如何復原的

以上面的例子來說,假設執行 rollback,那麼對應的流程應該是這樣:

(1)通過 undo no = 3 的日志把 id = 2 的資料删除;

(2)通過 undo no = 2 的日志把 id= 1 的資料的 deletemark 還原成 0;

(3)通過 undo no = 1 的日志把 id = 1 的資料的 name 還原成 Tom;

(4)通過 undo no = 0 的日志把 id = 1 的資料删除;

2.5.4.undo log 的删除

(1)針對于 insert undo log

因為 insert 操作的記錄,隻對事務本身可見,對其他事務不可見。故該 undo log 可以在事務送出後直接删除,不需要進行 purge 操作。

(2)針對于update undo log

該 undo log 可能需要提供 MVCC 機制,是以不能在事務送出時就進行删除。送出時放入 undo log 連結清單,等待 purge 線程進行最後的删除。

補充:purge 線程兩個主要作用是清理 undo 頁和清除 page 裡面帶有 Delete_Bit 辨別的資料行。在 InnoDB 中,事務中的 Delete 操作實際上并不是真正的删除掉資料行,而是一種 Delete Mark 操作,在記錄上辨別 Delete_Bit,而不删除記錄。是一種"假删除",隻是做了個标記,真正的删除工作需要背景 purge 線程去完成。

2.6.undo 小結

(1)undo log 是邏輯日志,對事務復原時,隻是将資料庫邏輯地恢複到原來的樣子。

(2)redo log 是實體日志,記錄的是資料頁的實體變化,undo log 不是 redo log 的逆過程。

MySQL 事務日志詳解

繼續閱讀