MySQL 的多版本併發控制(MVCC)是一種用於處理併發事務的機制,主要用於提高資料庫的效能和一致性。InnoDB 是 MySQL 中最常用的儲存引擎之一,它實現了 MVCC。
MVCC 的基本原理
資料版本化:在 MVCC 中,每行記錄都有多個版本。每當有事務對資料進行修改時,不是直接覆蓋舊的資料,而是建立一個新的版本。這些版本包含了必要的後設資料資訊,比如建立時間、過期時間等。
快照讀:大多數 SELECT 查詢都是使用快照讀,這意味著查詢會讀取到某個時間點的一致性檢視。透過這種方式,
讀操作不需要加鎖,可以極大地提高併發效能
。當前讀:對於更新操作,InnoDB 使用當前讀,即總是讀取最新版本的資料並對其進行加鎖以避免併發問題。
隱藏列:InnoDB 表中有兩個隱藏的列,用於 MVCC:
trx_id
: 記錄最近一次插入或更新該行的事務 ID。roll_pointer
: 指向撤銷日誌的指標,用於恢復舊版本資料。撤銷日誌(Undo Log) :儲存之前版本的資料,以便在需要時可以執行回滾,也為快照讀提供支援。
事務隔離級別:MVCC 支援多種事務隔離級別,常見的是 REPEATABLE READ。在這個級別下,事務開始時建立的快照在整個事務期間保持不變。
MVCC 快照的具體內容
事務 ID 列表
活躍事務列表:在快照建立時,正在執行的所有事務的 ID 列表。透過這個列表,InnoDB 可以知道哪些事務是活躍的,從而判斷當前事務可見的版本。
最小事務 ID (
min_trx_id
):在活躍事務列表中最小的事務 ID,即這些事務在快照建立之前已經啟動。最大事務 ID (
max_trx_id
):通常設定為系統中下一個將要分配的事務 ID,用於標識快照中的“未來”部分。可見性規則
使用快照時,InnoDB 依據以下規則判斷一個版本是否對當前事務可見:
如果
trx_id
屬於活躍事務列表,說明該事務尚未提交,因此該版本對當前事務不可見。如果
trx_id
不在活躍事務列表中,則該版本對當前事務可見。如果資料版本的
trx_id
小於min_trx_id
,則表示該版本是在快照建立前已經提交的,因此對當前事務可見。如果資料版本的
trx_id
大於或等於max_trx_id
,表示該版本在快照建立後生成,對當前事務不可見。如果資料版本的
trx_id
介於min_trx_id
和max_trx_id
之間,則需要進一步檢查:版本鏈及回滾指標
每條記錄都有多個版本,透過
roll_pointer
連結成一個版本鏈。當讀取一個記錄時,InnoDB 會沿著版本鏈查詢第一個符合可見性規則的版本。
MVCC 執行流程
在 MySQL 的 InnoDB 儲存引擎中,事務隔離級別為 REPEATABLE READ 時,MVCC 的實現流程主要涉及如何管理併發事務的讀取檢視和資料版本。以下是詳細流程:
1. 事務啟動
當一個事務以 REPEATABLE READ 隔離級別啟動時,InnoDB 會建立一個一致性檢視(Consistent Read View),這個檢視用於確保在整個事務過程中,讀操作能看到的是事務啟動時刻的一致性資料狀態。
2. 快照讀
讀取檢視:在事務開始時建立的讀取檢視會捕獲當前資料庫的狀態,包括已提交事務和正在進行中的事務。
版本鏈:每個錶行記錄都有一個隱藏列
trx_id
和一個回滾指標roll_pointer
。透過這些資訊,InnoDB 可以遍歷資料版本鏈,從而找到符合當前事務可見性的版本。可見性判斷:當事務要讀取某一行資料時,透過
trx_id
和讀取檢視來判斷該版本是否對當前事務可見。如果不可見,則沿著roll_pointer
找到上一個版本進行同樣的判斷,直到找到一個可見版本或無版本可用。
3. 當前讀
當事務執行更新、刪除或插入操作時,會進行當前讀。這種情況下,InnoDB 會鎖住最新版本的資料,並確保其他事務不會同時修改這部分資料。
4. 版本控制
插入操作:新插入的行附帶當前事務
trx_id
作為其版本。更新操作:生成一個新版本,舊版本保持不變並透過
roll_pointer
保持連結。刪除操作:標記當前版本為刪除,新版本不會被建立。
5. 事務提交與回滾
提交:事務提交後,其所做的更改對於之後啟動的事務立即可見。
回滾:如果事務回滾,根據
undo log
恢復資料到之前的狀態。
6. 幻讀處理
在 REPEATABLE READ 下,InnoDB 透過 Next-Key Locking 技術處理幻讀問題,它不僅鎖定存在的行,還會鎖住索引範圍,防止插入導致幻影行出現。
Next-Key Locking 結合了行鎖(Record Lock)和間隙鎖(Gap Lock)。它不僅鎖住了索引記錄本身,還鎖住了這些記錄之間的空隙。
案例說明
假設有一個表 accounts
,包含以下欄位:id
, name
, 和 balance
。
初始狀態
表中有一條記錄:
id | name | balance |
---|---|---|
1 | Alice | 1000 |
版本鏈操作過程
事務 T1 開始
trx_id
: 9資料內容:
(id=1, name='Alice', balance=1000)
T1 開始時間戳為 10,讀取
accounts
。當前資料版本:
事務 T2 開始並更新
trx_id
: 11資料內容:
(id=1, name='Alice', balance=1200)
T2 開始時間戳為 11,對
balance
進行更新:UPDATE accounts SET balance = 1200 WHERE id = 1;
新資料版本(未提交):
舊資料版本仍然存在,形成版本鏈。
事務 T3 開始並提交更新
T3 開始時間戳為 12,插入新記錄:
INSERT INTO accounts (id, name, balance) VALUES (2, 'Bob', 500);
提交後,生成新的已提交版本。
事務 T1 查詢
T1 的開始時間為 10,它只看得到
trx_id
小於等於 10 的版本。因此,T1 看到的是舊版本
(id=1, name='Alice', balance=1000)
。T1 再次查詢
accounts
:SELECT * FROM accounts WHERE id = 1;
可見性判斷:
事務 T2 提交
已提交版本:
(id=1, name='Alice', balance=1200, trx_id: 11)
T2 提交時,更新的版本
(trx_id: 11)
成為有效版本。數據鏈變為:
新事務 T4 開始並查詢
T4 的開始時間為 13,它可以看到所有在時間戳 13 前提交的版本。
於是,T4 看到最新的已提交版本
(id=1, name='Alice', balance=1200)
。T4 開始時間戳為 13,讀取
accounts
:SELECT * FROM accounts WHERE id = 1;
可見性判斷:
總結
版本鏈:每次資料修改都會產生一個新版本,舊版本透過
roll_pointer
連結起來,形成版本鏈。可見性判斷:每個事務只能看到其開始之前提交的版本,透過對比事務的開始時間戳與行的
trx_id
來決定可見性。
回滾流程
在 MySQL 的 InnoDB 儲存引擎中,事務回滾操作會影響版本鏈的狀態。回滾發生時,未提交的修改將被撤銷,並且對最終的資料可見性不會產生影響。這是透過撤銷日誌(undo log)和版本鏈的管理實現的。
事務回滾過程中版本鏈的變化
場景描述
假設我們有一個表 accounts
,其中包含以下記錄:
id | name | balance |
---|---|---|
1 | Alice | 1000 |
操作步驟
事務 T1 開始
新版本(未提交):
(id=1, name='Alice', balance=1200, trx_id=T1)
舊版本:
(id=1, name='Alice', balance=1000)
T1 對記錄進行更新:
UPDATE accounts SET balance = 1200 WHERE id = 1;
此時,InnoDB 會為該記錄生成一個新的未提交的版本。
版本鏈:
事務 T1 回滾
有效版本:
(id=1, name='Alice', balance=1000)
在 T1 決定回滾的時候,系統會使用 undo log 將資料恢復到它執行更新之前的狀態。
未提交的新版本
(trx_id=T1)
被丟棄,不再存在於版本鏈中。舊版本成為當前有效版本:
回滾後的結果
在 T1 回滾完成後,其他事務或查詢看到的都是回滾前的狀態,即
balance=1000
的記錄。由於 T1 的更改從未真正被提交到資料庫,所以不會有任何其他事務看到其更改過的版本。
總結
Undo Log:回滾時使用 undo log 將資料恢復到事務開始之前的狀態。這是 MVCC 實現一致性快照和允許事務回滾的關鍵。
版本鏈維護:當事務未提交時,它的更改僅對自己可見。回滾後這些更改被撤銷,未提交的版本從版本鏈中移除。
資料一致性:回滾保證了即使事務發生錯誤或被使用者取消,資料庫依然能保持一致的狀態。
思考題:RR與RC效能對比
影響效能的因素
鎖爭用:
在高併發寫操作下,REPEATABLE READ 由於使用間隙鎖(gap locking),可能導致更多的鎖爭用,從而增加事務等待時間。
READ COMMITTED 由於僅鎖定當前訪問的行,因此在同等條件下通常能提供更好的併發效能。
應用場景:
對於需要穩定檢視以確保業務邏輯正確性的場景(例如,複雜的報表或分析查詢),REPEATABLE READ 可以避免因資料變化所產生的不一致結果。
在實時性要求較高且允許輕微不一致的場景(例如,一些線上系統的瀏覽操作),READ COMMITTED 提供的靈活性和效能優勢可能更明顯。