之前文章:Mysql鎖_exclusivelock for update寫鎖-CSDN博客? ?中有提到通過MVCC來實現快照讀,從而解決幻讀問題,這里詳細介紹下MVCC。
一、前言
id | k |
1 | 1 |
2 | 2 |
事務A | 事務B | 事務C |
start transaction with consistent snaption | ||
start transaction with consistent snaption | ||
update t set k=k+1 where id =1 | ||
update t set k=k+1 where id =1; select k from t where id =1; | ||
select k from t where id =1; commit; | ||
commit |
先看上面執行流程,先思考下事務A和B兩次查詢結果都是什么。
注:
1、begin/start transaction 命令并不是一個事務的起點,在執行到它們之后的第一個操作InnoDB表的語句(第一個快照讀語句),事務才真正啟動。這里使用start transaction with consistent snaption命令立即開始事務。
2、事務C沒有使用命令開啟事務,因為update語句本身就是一個事務,執行完畢后執行commit
二、MVCC 的核心思想
????????MVCC 的核心是通過?數據多版本?和?一致性視圖(Consistent Read View)?來實現高并發下的讀寫隔離。其核心思想是:
-
每個數據行有多個版本,每次更新生成新版本,舊版本通過 undo log 保留。
-
事務根據可見性規則判斷應讀取哪個版本,而非直接讀取最新數據。
1. 事務ID與行版本
-
事務ID(Transaction ID):每個事務啟動時,InnoDB會為其分配一個全局唯一且遞增的ID(
trx_id
)。 -
行數據的版本:每次事務修改數據時,會生成一個新的數據版本,并將事務ID記錄在該版本的?
row trx_id
?字段中。舊版本的數據通過?Undo Log?保存,形成版本鏈。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 圖1:行狀態變更圖
????????上圖就是一個記錄被多個事務連續更新后的狀態。圖中虛線框里是同一行數據的4個版本,當前最新版本是V4,k的值是22,它是被transaction id 為25的事務更新的,因此它的row trx_id也是25。U1,U2,U3則是undo log的記錄的日志。
2. 一致性讀視圖(Consistent Read View)
???????事務啟動時(RR 級別)或語句執行時(RC 級別),InnoDB 會生成一個?一致性視圖,用于判斷數據版本的可見性。
Read View 的四大核心屬性
1. trx_ids(活躍事務 ID 集合)
- 含義:生成 Read View 時,當前系統中所有未提交的活躍事務 ID 的集合。
- 作用:用于判斷數據版本的事務是否在 Read View 生成時處于活躍狀態。若在集合中,則該版本對當前事務不可見(除了自身事務,自身事務對于表的修改對于自己當然是可見的)。
2. up_limit_id(最小活躍事務 ID)
- 含義:trx_ids集合中的最小事務 ID。
- 作用:若數據版本的事務 ID < low_trx_id → 該版本在 Read View 生成前已提交,可見。
3. low_limit_id(最大事務 ID 上限)
- 含義:生成 Read View 時,系統中尚未分配的下一個事務 ID(并非實際存在的事務 ID)。
- 作用:若數據版本的事務 ID ≥ up_trx_id → 該版本在 Read View 生成后才被創建,不可見。
4.?creator_trx_id(當前事務 ID)
- 含義:生成該 Read View 的當前事務 ID。
- 作用:若數據版本的事務 ID == creator_trx_id → 當前事務自己修改的數據,可見。
數據可見性判斷規則
當事務讀取一行數據時,需根據以下條件判斷版本是否可見:
- 版本事務 ID < up_limit_id?→ 可見(已提交且早于 Read View 生成)。
- 版本事務 ID >= low_limit_id?→ 不可見(生成時間晚于 Read View)。
- 版本事務 ID 在 [up_limit_id, low_limit_id) 區間內:
- 若在 trx_ids中 → 不可見(活躍未提交)。
- 若不在 trx_ids中 → 可見(已提交且在 Read View 生成后提交)。
- 版本事務 ID == creator_trx_id?→ 可見(當前事務自己修改的)。
注意:一旦一個Read View被創建,這三個參數將不再發生變化,其中low_limit_id 和 up_limit_id分別是 trx_Ids數組的上下界(注意:從單詞上來區分的話很容易弄反)。
三、MVCC 如何實現隔離級別
1. 可重復讀(RR)
-
視圖創建時機:事務啟動時創建一致性視圖,后續所有讀操作基于此視圖。
-
效果:事務內看到的數據始終一致,不受其他事務提交的影響。
示例分析:
如上面表格2中:
假設事務開始前,當前活躍事務id=99,則事務A、B、C的事務id依次是100、101、102,事務開始前id=1 k=1的這一行數據的row trx_ids是90。(版本V1)
那么,我們看下在 RR 隔離級別下執行過程:
視圖建立時,事務A的trx_ids=[99,100],同樣事務B的trx_ids=[99,100,101]、事務C的trx_ids=[99,100,101,102]。先執行事務C的update,當前版本從V1(k=1)變成V2(k=2),V1則變為歷史版本,執行事務B的update,歷史版本從V2(k=2)變成V3(k=3),V2變成歷史版本。
這里為啥執行事務B,從k=2變成3,不是事務隔離嗎?這個我們在后面解釋。
事務B執行查詢操作時:
trx_ids: [99,100,101]
up_limit_id: [99]
low_limit_id: [102]
creator_trx_id=101,先查看V3版本,事務id=101在trx_ids中,但是等于我們當前事務,所以可見,所以最后結果k=3?
事務A執行查詢操作時:
trx_ids: [99,100]
up_limit_id: [99]
low_limit_id: [101]
creator_trx_id=100,先查看V3版本,事務id=101 >=?low_limit_id 不可見,然后查找V2版本,事務id=102 >=?low_limit_id,不可見,在查找V1版本,事務id=90,不在trx_ids中,可見。所有我們通過undo log,從V3 -> V2 -> V1,我們獲取數據,最后結果k=1
這樣執行下來,雖然期間這一行數據被修改過,但是事務A不論在什么時候查詢,看到這行數據的結果都是一致的,所以我們稱之為一致性讀。
2. 讀提交(RC)
-
視圖創建時機:每條語句執行前重新生成一致性視圖。
-
效果:每次查詢能看到已提交的最新數據。
示例分析
表2中的“start transaction with consistent snapshot; ”的意思是從這個語句開始,創建一個持續整個事務的一致性快照。所以,在讀提交隔離級別下,這個用法就沒意義了,等效于普通的start transaction。
在 RC 隔離級別下:
事務B的查詢結果和RR一致。
-
事務 C?已提交(ID=102),不在活躍事務數組中。
-
事務 B?未提交(ID=101),仍在活躍事務數組中。
-
事務 A?執行查詢時:
-
trx_ids:
[101]
(僅事務 B 未提交)。 -
up_limit_id:101(活躍事務最小 ID)。
-
low_limit_id:103(當前最大事務 ID=102,+1 后為 103)。
數據可見性判斷:
-
若數據的最新版本由事務 B(ID=101)更新:
-
row trx_id=101
?在活躍數組中,不可見。 -
繼續查找歷史版本,找到事務 C(ID=102)提交的版本:
-
row trx_id=102 < 高水位(103)
,且不在活躍數組中,可見。
-
-
-
因此,事務 A 的第一次查詢會讀到事務 C 提交后的數據,即k=2。
-
如果事務B提交后,事務A再執行一次查詢呢?
?事務 B 提交后
-
事務 B?提交后,ID=101 不再屬于活躍事務。
-
事務 A?執行第二次查詢:
-
活躍事務數組:
[]
(無未提交事務)。 -
低水位:無(數組為空)。
-
高水位:103(最大事務 ID 仍為 102,+1 后為 103)。
數據可見性判斷:
-
數據的最新版本由事務 B(ID=101)提交:
-
row trx_id=101 < 高水位(103)
,且不在活躍數組中,可見。
-
-
因此,事務 A 的第二次查詢會讀到事務 B 提交后的數據。
-
四、當前讀與一致性讀
MVCC 的讀操作分為兩種模式:
-
一致性讀(Consistent Read):基于視圖讀取歷史版本,用于普通?
SELECT
。 -
當前讀(Current Read):讀取最新數據并加鎖,用于更新操作(如?
UPDATE
、SELECT ... FOR UPDATE
)。
為什么更新需要當前讀?
假設事務 B 要更新數據:
-
若使用一致性讀,可能基于舊版本數據計算新值,導致其他事務的更新丟失。
-
因此,更新操作必須讀取最新版本(當前讀),并對記錄加鎖,確保數據一致性。
所以這里可以解釋,為什么事務B執行update操作k是從2變成3,讀到了事務C提交的數據,因為在更新的時候,當前讀拿到的數據是(k=2),更新后生成了新版本的數據(k=3),這個新版本的row trx_id是101。所以,在執行事務B查詢語句的時候,一看自己的版本號是101,最新數據的版本號也是101,是自己的更新,可以直接使用,所以查詢得到的k的值是3。
五、案例分析
除了文章開始案例,我們這里再列舉幾個案例分析下。
案例1
事務A | 事務B | 事務C |
start transaction with consistent snaption | ||
start transaction with consistent snaption | ||
start transaction with consistent snaption; update t set k=k+1 where id =1; | ||
update t set k=k+1 where id =1; select k from t where id =1; | ||
select k from t where id =1; commit; | commit | |
commit |
我們看上面實例,跟前面分析相比,事務C執行update操作后并沒有立即提交,那么如何執行呢。
這里我們需要介紹二階段協議:
?在InnoDB事務中,行鎖是在需要的時候才加上的,但并不是不需要了就立刻釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議。
執行流程:
-
事務C啟動:
-
更新?
id=1
?的行,將?k
?從?1
?改為?2
,但未提交(持有行鎖)。 -
此時數據版本鏈為:
V1(row trx_id=90, k=1)
?→?V2(row trx_id=102, k=2)
(未提交)。
-
-
事務B啟動:
-
嘗試執行?
UPDATE t SET k=k+1 WHERE id=1
,需要獲取行鎖。 -
因事務C未提交,事務B被阻塞,進入鎖等待狀態。
-
-
事務A啟動(RR隔離級別):
-
執行?
SELECT k?FROM t WHERE id=1
。 -
根據一致性視圖規則,事務A的視圖數組包含啟動時活躍事務(如事務C的ID=102)。
-
數據版本鏈中,
V2
?的?row trx_id=102
?在活躍事務數組中,不可見;最終讀取?V1
(k=1
)。
-
-
事務C提交:
-
提交后釋放行鎖,數據版本?
V2
?的?row trx_id=102
?變為已提交。 -
事務B獲得鎖,執行當前讀,讀取最新版本?
V2
(k=2
),更新為?k=3
,生成新版本?V3(row trx_id=101)
。
-
-
事務A再次查詢:
-
仍基于啟動時的視圖,不可見事務B和C'的提交,結果仍為?
k=1
。
-
-
事務B提交:
-
提交后數據版本?
V3
?的?row trx_id=101
?變為已提交。 -
新事務查詢會看到?
k=3
。
-
注意:上面沒有死鎖風險,因為只有事務C和事務B在競爭同一行的鎖,且是單向等待(事務B等待事務C釋放鎖),無循環依賴,因此不會死鎖。
案例2
事務A | 事務B | 事務C |
start transaction with consistent snaption | ||
start transaction with consistent snaption | ||
start transaction with consistent snaption; update t set k=k+1 where id =1; | ||
update t set k=k+1 where id =2; | ||
select k from t where id =1; commit; | update t set k=k+1 where id =2; | |
update t set k=k+1 where id =1; | ||
commit | commit |
如果出現上面場景呢?
執行流程
-
事務C:更新行id=1?→ 持有行1的鎖。
-
事務B:更新行id=2 → 持有行2的鎖。
-
事務C:嘗試更新行2 → 等待事務B釋放行2的鎖。
-
事務B:嘗試更新行1?→ 等待事務C釋放行1的鎖。
此時,事務B和事務C互相等待對方釋放資源,形成循環依賴,觸發死鎖。
死鎖相關分析可以參考:Mysql死鎖_mysql 死鎖的條件-CSDN博客
案例3
事務A | 事務B |
begin | |
select k from t | |
update t set k=k+1 | |
update t set k=0 where id = k; select k from t; |
上面運行結果:數據庫會拒絕事務A的修改(如報錯或阻塞),“數據無法修改”。
六、MVCC 的優缺點
優點
-
高并發:讀寫不互相阻塞,讀操作無需加鎖。
-
避免臟讀和不可重復讀:通過版本鏈和可見性規則實現隔離。
缺點
-
存儲開銷:需保留多個數據版本和 undo log。
-
長事務問題:長事務可能導致大量歷史版本無法清理,占用存儲空間。
六、實際應用建議
-
避免長事務:監控?
information_schema.innodb_trx
,及時終止長時間未提交的事務。 -
優先使用 RC 隔離級別:若業務允許,RC 比 RR 更節省資源。
-
更新前顯式加鎖:如需確保數據一致性,使用?
SELECT ... FOR UPDATE
?明確加鎖。