MySQL系列(8)— 事務原子性之UndoLog

專欄系列文章:MySQL系列專欄mysql

Undo Log

事務的第一個特性就是原子性,原子性就是要保證一個事務中的增刪改操做要麼都成功,要麼都不作。這時就須要 undo log,在對數據庫進行修改前,會先記錄對應的 undo log,而後在事務失敗或回滾的時候,就能夠用這些 undo log 來將數據回滾到修改以前的樣子。web

下面先簡單介紹下事務ID和行記錄中的隱藏列,由於後面的內容都與這兩個東西有關係。sql

事務ID

事務執行過程當中在對某個表執行增、刪、改操做時,InnoDB就會給這個事務分配一個惟一的事務ID。若是一個事務中沒有執行增刪改操做,就不會分配事務ID。數據庫

InnoDB 在內存維護了一個全局變量來表示事務ID,每當要分配一個事務ID時,就獲取這個變量值,而後把這個變量自增1。每當這個變量的值爲256的倍數時,就會將該變量的值刷新到系統表空間的頁號爲5的頁面中一個稱之爲Max Trx ID的屬性處。當系統下一次從新啓動時,會將Max Trx ID屬性加載到內存中,並將該值加上256以後賦值給這個全局變量(這個過程跟主鍵row_id的分配是相似的)。markdown

咱們能夠經過 information_schema.INNODB_TRX 來查詢當前系統中運行的事務信息,這張表的第一個字段trx_id就是事務ID。數據結構

mysql> SELECT trx_id,trx_state,trx_started,trx_rows_locked,trx_isolation_level,trx_is_read_only FROM information_schema.INNODB_TRX;
+-----------+-----------+---------------------+-----------------+---------------------+------------------+
| trx_id    | trx_state | trx_started         | trx_rows_locked | trx_isolation_level | trx_is_read_only |
+-----------+-----------+---------------------+-----------------+---------------------+------------------+
| 164531720 | RUNNING   | 2021-05-14 16:38:59 |               1 | REPEATABLE READ     |                0 |
+-----------+-----------+---------------------+-----------------+---------------------+------------------+
複製代碼

行記錄隱藏列

在介紹InnoDB行記錄格式這篇文章中,咱們瞭解到行記錄中會有三個隱藏列:併發

  • DB_ROW_ID:若是沒有爲表顯式的定義主鍵,而且表中也沒有定義惟一索引,那麼InnoDB會自動爲表添加一個row_id的隱藏列做爲主鍵。
  • DB_TRX_ID:事務中對某條記錄作增刪改時,就會將這個事務的事務ID寫入trx_id中。
  • DB_ROLL_PTR:回滾指針,本質上就是指向 undo log 的指針。

image.png

Undo Log 類型

每對一條記錄作一次改動,就會產生1條或者2undo log。一個事務中可能會有多個增刪改SQL語句,一個SQL語句可能會產生多條 undo log,一個事務中的這些 undo log 會被從 0 開始遞增編號,這個編號稱爲 undo no高併發

undo log 主要是記錄對數據庫增刪改的撤銷日誌,下面就分別來看下增刪改操做的 undo log 格式是怎樣的。url

仍是以以前的 account 表爲例來作一些演示。spa

CREATE TABLE `account` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `card` varchar(60) NOT NULL COMMENT '卡號',
  `balance` int(11) NOT NULL DEFAULT '0' COMMENT '餘額',
  PRIMARY KEY (`id`),
  UNIQUE KEY `account_u1` (`card`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='帳戶表';
複製代碼

咱們能夠經過 information_schema.INNODB_SYS_TABLES 查詢獲得這張表的表空間ID爲 13881

mysql> SELECT * FROM information_schema.INNODB_SYS_TABLES WHERE name = 'test/account';
+----------+-------------+------+--------+-------+-------------+------------+---------------+------------+
| TABLE_ID | NAME        | FLAG | N_COLS | SPACE | FILE_FORMAT | ROW_FORMAT | ZIP_PAGE_SIZE | SPACE_TYPE |
+----------+-------------+------+--------+-------+-------------+------------+---------------+------------+
|    13881 | test/account |   33 |      6 | 13935 | Barracuda   | Dynamic    |             0 | Single     |
+----------+-------------+------+--------+-------+-------------+------------+---------------+------------+
複製代碼

insert undo

插入一條數據對應的undo操做其實就是根據主鍵刪除這條數據就好了。因此 insert 對應的 undo log 主要是把這條記錄的主鍵記錄上。

INSERT 產生的 undo log 類型爲 TRX_UNDO_INSERT_REC,大體結構以下圖所示:

  • start、end:指向記錄開始和結束的位置。
  • undo type:undo log 的類型,也就是 TRX_UNDO_INSERT_REC
  • undo no:在當前事務中 undo log 的編號。
  • table id:表空間ID。
  • 主鍵列信息:這一塊就須要記錄INSERT這行數據的主鍵ID信息,或者惟一列信息。

image.png

好比咱們開啓了一個事務,向 account 中插入兩條數據:

BEGIN;

INSERT INTO account(id,card,balance) VALUES (1, 'AA', 0),(2, 'BB', 0);
複製代碼

假設這個事務的事務ID爲100,這條INSERT語句會插入兩條數據,就會產生兩個 undo log。插入記錄的時候,會在行記錄的隱藏列事務ID中寫入當前事務ID,併產生 undo log,記錄中的回滾指針會保存 undo log 的地址。而同一個頁中的多條記錄會經過next_record鏈接起來造成一個單鏈表,這塊能夠參考前面的行記錄格式和數據頁結構相關的文章。

image.png

delete undo

刪除一條數據大體能夠分爲兩個階段:

  • 階段一

首先是用戶線程執行刪除時,會先將記錄頭信息中的 delete_mask 標記爲 1,而不是直接從頁中刪除,由於可能其它併發的事務還須要讀取這條數據。(後面講MVCC的時候就知道爲何了)

  • 階段二

提交事務後,後臺有一個 purge 線程會將數據真正刪除。

首先要知道,頁中的數據是經過記錄頭信息中的 netx_record 鏈接起來的單向鏈表(假設這個鏈表稱爲數據鏈表)。頁中還有另外一個鏈表,稱爲垃圾鏈表,記錄真正刪除後,會從數據鏈表中移除,而後加入到垃圾鏈表的頭部,以便重用空間。

因此階段二就是將記錄從數據鏈表移除,加入到垃圾鏈表的頭部。

也就是說,刪除操做在事務提交前,只會經歷階段一,就是將記錄的 delete_mask 標記爲 1

DELETE 對應的 undo log 類型爲 TRX_UNDO_DEL_MARK_REC,它的結構大體以下圖所示,與 TRX_UNDO_INSERT_REC 類型相比,主要多了三個部分:

  • old trx_id:這個屬性會保存記錄中的隱藏列trx_id,這個屬性在MVCC併發讀的時候就會起做用了。
  • old roll_pointer:這個屬性保存記錄中的隱藏列roll_pointer,這樣就能夠經過這個屬性找到以前的 undo log。
  • 索引列信息:這部分主要是在第二階段事務提交後用來真正刪除記錄的。

image.png

此時接着執行一條刪除的SQL語句,將id=2的這條數據刪除:

BEGIN;

INSERT INTO account(id,card,balance) VALUES (1, 'AA', 0),(2, 'BB', 0);

DELETE FROM account WHERE id = 2;
複製代碼

由於是在同一個事務中,因此記錄中的隱藏列trx_id沒變,記錄頭中的delete_mask則標記爲1了。而後生成了一個新的 undo log,並保存了記錄中本來的trx_idroll_pointer,因此這個新的 undo log 就指向了舊的 undo log,而記錄中的 roll_pointer 則指向這個新的 undo log。注意 undo log 中的事務編號也在遞增。

image.png

update undo

在執行UPDATE語句時,InnoDB對更新主鍵和不更新主鍵這兩種狀況有大相徑庭的處理方案,對應中兩種不一樣的 undo log 類型。

不更新主鍵

在不更新主鍵的狀況下,又能夠細分爲被更新的列佔用的存儲空間不發生變化和發生變化的狀況。

  • 存儲空間未發生變化

更新記錄時,對於被更新的每一個列來講,若是更新後的列和更新前的列佔用的字節數都同樣大,那麼就能夠進行就地更新,也就是直接在原記錄的基礎上修改對應列的值。

  • 存儲空間發生變化

若是有任何一個被更新的列更新前和更新後佔用的字節數大小不一致,那麼就會先把這條舊的記錄從聚簇索引頁面中刪除掉,而後再根據更新後列的值建立一條新的記錄插入到頁面中。注意這裏的刪除並非將 delete_mask 標記爲 1,而是真正的刪除,從數據鏈表中移除加入到垃圾鏈表的頭部。

若是新的記錄佔用的存儲空間大小不超過舊記錄佔用的空間,就能夠直接重用剛加入垃圾鏈表頭部的那條舊記錄所佔用的空間,否就會在頁面中新申請一段空間來使用。

不更新主鍵的這兩種狀況生成的 undo log 類型爲 TRX_UNDO_UPD_EXIST_REC,大體結構以下圖所示,與 TRX_UNDO_DEL_MARK_REC 相比主要是多了更新列的信息。

image.png

假設此時更新id=1的這條數據,各列佔用的字節大小都未變化:

BEGIN;

INSERT INTO account(id,card,balance) VALUES (1, 'AA', 0),(2, 'BB', 0);

DELETE FROM account WHERE id = 2;

UPDATE account SET card = 'CC' WHERE id = 1;
複製代碼

這條記錄就會執行就地更新,一樣會產生一條新的 undo log,並指向原來的 undo log。

image.png

更新主鍵

要知道記錄是按主鍵大小連成一個單向鏈表的,若是更新了某條記錄的主鍵值,這條記錄的位置也將發生改變,也許就被更新到其它頁中了。

這種狀況下的更新分爲兩步:

  • 首先將原記錄作標記刪除,就是將 delete_mask 改成 1,尚未真正刪除。
  • 而後再根據更新後各列的值建立一條新記錄,並將其插入到聚簇索引中。

因此這種狀況下,會產生兩條 undo log:

  • 第一步標記刪除時會建立一條 TRX_UNDO_DEL_MARK_REC 類型的 undo log。
  • 第二步插入記錄時會建立一條 TRX_UNDO_INSERT_REC 類型的 undo log。

這兩種類型的結構前面已經說過了。

此時再將id=1的主鍵更新:

BEGIN;

INSERT INTO account(id,card,balance) VALUES (1, 'AA', 0),(2, 'BB', 0);

DELETE FROM account WHERE id = 2;

UPDATE account SET card = 'CC' WHERE id = 1;

UPDATE account SET id = 3 WHERE id = 1;
複製代碼

更新主鍵後,本來的記錄就被標記刪除了,而後新增了一個 TRX_UNDO_DEL_MARK_REC 的 undo log。接着插入了一條新的id=3的記錄,並建立了一個新的 TRX_UNDO_INSERT_REC 類型的 undo log。

image.png

undo log 回滾

前面在一個事務中增刪改產生的一系列 undo log,都有 undo no 編號的。在回滾的時候,就能夠應用這個事務中的 undo log,根據 undo no 從大到小開始進行撤銷操做。

例如上面的例子若是最後回滾了:

  • 就會先執行第 5 號 undo log,刪除 id=3 這條數據;
  • 接着第4號 undo log,取消標記刪除,將 id=1 這條數據的 delete_mask 改成 0
  • 接着第3號 undo log,將更新的列card='CC'還原爲原來的card='AA'
  • 接着第2號 undo log,取消標記刪除,將 id=2 這條數據的 delete_mask 改成 0
  • 接着第1號 undo log,刪除 id=2 這條數據;
  • 接着第0號 undo log,刪除 id=1 這條數據;

能夠看到,回滾時經過執行 undo log 撤銷,就將數據還原爲原來的樣子了。

但須要注意的是,undo log 是邏輯日誌,只是將數據庫邏輯地恢復到原來的樣子。全部修改都被邏輯地取消了,可是數據結構和頁自己在回滾以後可能大不相同。由於同時可能不少併發事務在對數據庫進行修改,所以不能將一個頁回滾到事務開始的樣子,由於這樣會影響其餘事務正在進行的工做。

Undo Log 存儲

undo log 分類

前邊介紹了幾種類型的 undo log,它們其實被分爲兩個大類來存儲:

  • TRX_UNDO_INSERT

類型爲 TRX_UNDO_INSERT_REC 的 undo log 屬於此大類,通常由 INSERT 語句產生,或者在 UPDATE 更新主鍵的時候也會產生。

  • TRX_UNDO_UPDATE

除了類型爲 TRX_UNDO_INSERT_REC 的 undo log,其餘類型的 undo log 都屬於這個大類,好比 TRX_UNDO_DEL_MARK_REC 、 TRX_UNDO_UPD_EXIST_REC ,通常由 DELETE、UPDATE 語句產生。

之因此要分紅兩個大類,是由於不一樣大類的 undo log 不能混着存儲,由於類型爲TRX_UNDO_INSERT_REC的 undo log 在事務提交後能夠直接刪除掉,而其餘類型的 undo log 還須要提供MVCC功能,不能直接刪除。

undo 頁面鏈表

undo log 是存放在FIL_PAGE_UNDO_LOG類型的頁中,一個事務中可能會產生不少 undo log,也許就須要申請多個undo頁,因此 InnoDB 將其設計爲一個鏈表的結構,將一個事務中的多個undo頁鏈接起來。

可是前面說了 undo log 分爲兩大類,不能混着存儲,因此若是事務中產生了這兩大類型的 undo log,會建立兩個鏈表,一個用來存儲 TRX_UNDO_INSERT 類別的 undo log,一個用來存儲 TRX_UNDO_UPDATE 類別的 undo log。

若是事務中還修改了臨時表,InnoDB規定對普通表和臨時表修改產生的 undo log 要分開存儲,因此在一個事務中最多可能會有4個 undo 頁面鏈表。

須要注意的是這些鏈表並非事務一開始就分配好的,而是在須要某個類型的鏈表的時候纔會去分配。

重用 undo 頁

若是有多個併發事務執行,爲了提升 undo log 的寫入效率,不一樣事務執行過程當中產生的 undo log 會被寫入到不一樣的 undo 頁面鏈表中。也就是說一個事務最多可能單獨分配4個鏈表,兩個事務可能就8個鏈表。

但其實大部分事務都是一些短事務,產生的 undo log 不多,這些 undo log 只會佔用一個頁少許的存儲空間,這樣就會很浪費。因而 InnoDB 設計在事務提交後,在某些狀況下能夠重用這個事務的 undo 頁面鏈表。

undo 鏈表能夠被重用的條件:

  • 在 undo 頁面鏈表中只包含一個 undo 頁面時,該鏈表才能夠被下一個事務所重用。由於若是一個事務產生了不少 undo log,這個鏈表就可能有多個頁面,而新事務可能只使用這個鏈表不多的一部分空間,這樣就會形成浪費。
  • 而後該 undo 頁面已經使用的空間小於整個頁面空間的 3/4時才能夠被重用。

對於TRX_UNDO_INSERT類型的 insert undo 頁面鏈表,這些 undo log 在事務提交以後就沒用了,能夠被清除掉。因此在某個事務提交後,重用這個鏈表時,能夠直接覆蓋掉以前的 undo log。

對於TRX_UNDO_UPDATE類型的 update undo 頁面鏈表,這些 undo log 在事務提交後,不能當即刪除掉,由於要用於MVCC。因此重用這個鏈表時,只能在後面追加 undo log,也就是一個頁中可能寫入多組 undo log。

回滾段

redo log 是存放在重作日誌文件中的,而 undo log 默認是存放在系統表空間中的一個特殊段(segment)中,這個段稱爲回滾段(Rollback Segment),鏈表中的頁面都是從這個回滾段裏邊申請的。

爲了更好的管理系統中的 undo 頁面鏈表,InnoDB 設計了一個 Rollback Segment Header 的頁面,每一個Rollback Segment Header頁面都對應着一個Rollback Segment。一個 Rollback Segment Header 頁面中包含1024undo slot,每一個 undo slot 存放了 undo 鏈表頭部的 undo 頁的頁號。

一個 Rollback Segment Header 只有 1024 個 undo slot,假設一個事務中只分配了1個undo鏈表,那最多也只能支持1024個併發事務同時執行,在現今高併發狀況下,這顯然是不夠的。

因此InnoDB定義了128個回滾段,也就有128個 Rollback Segment Header,就有128*1024=131072undo slot,也就是說最多同時支持131072個併發事務執行。

在系統表空間的第5號頁面中存儲了這128Rollback Segment Header頁面地址。

能夠經過以下幾個參數對回滾段作配置:

  • innodb_undo_directory:undo log 默認存放在系統表空間中,也能夠配置爲獨立表空間。能夠經過這個參數設置獨立表空間的目錄,默認是數據目錄。

  • innodb_undo_logs:設置回滾段的數量,默認是128。但須要注意的是,針對臨時表的回滾段數量固定爲32個,那麼針對普通表的回滾段數量就是這個參數值減去32,若是設置小於32的值,就只有1個針對普通表的回滾段。

  • innodb_undo_tablespaces:設置undo表空間文件的數量,這樣回滾段能夠較爲平均的分佈到多個文件中。該參數默認爲0,表示不建立undo獨立表空間。

mysql> SHOW VARIABLES LIKE 'innodb_undo%';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_undo_directory    | .\    |
| innodb_undo_logs         | 128   |
| innodb_undo_tablespaces  | 0     |
+--------------------------+-------+
複製代碼

恢復 undo

undo log 寫入 undo 頁後,這個頁就變成髒頁了,也會加入 Flush 鏈表中,而後在某個時機刷到磁盤中。

事務提交時會將 undo log 放入一個鏈表中,是否能夠最終刪除 undo log 及 undo log 所在頁,是由後臺的一個 purge 線程來完成的。

最後也是最爲重要的一點是,undo log 寫入 undo 頁的時候也會產生 redo log,由於 undo log 也須要持久性的保護。

這裏其實要說的的是前面 redo log 未解決的一個問題。

仍是這張T一、T2併發事務的圖,在圖中箭頭處,若是T1事務執行完成提交事務,此時 redo log 就會刷盤。而T2事務還未執行完成,但它的 mtr_T2_1 已經刷入磁盤了。若是此時數據庫宕機了,T2事務實際是執行失敗的。在重啓數據庫後,就會讀取 mtr_T2_1 來恢復數據,而T2事務實際是未完成的,因此這裏恢復數據就會致使數據有問題。

image.png

因此這時 undo log 就派上用場了,redo log 恢復時,一樣會對 undo 頁重作,mtr_T2_1 這段 redo log 對數據頁重作後,因爲T2事務未提交,就會用 undo log 來撤銷這些操做。就解決了這個問題。

相關文章
相關標籤/搜索