一、什么是MVCC?
MVCC(Multi-Version Concurrency Control,多版本并發控制)是數據庫中用于解決并發訪問問題的一種機制。它通過為數據維護多個版本,讓讀寫操作在不同版本上獨立進行,從而避免了傳統鎖機制中讀寫沖突導致的性能損耗(如讀操作不會阻塞寫操作,寫操作也不會阻塞讀操作)。
MVCC的核心目標是:在保證事務隔離性的前提下,提高數據庫的并發性能。它廣泛應用于支持行級鎖的數據庫(如MySQL的InnoDB、PostgreSQL等),尤其在讀已提交(Read Committed) 和可重復讀(Repeatable Read) 隔離級別中發揮關鍵作用。
二、MVCC的實現原理
MVCC的實現依賴于三個核心組件:數據的隱藏列、回滾日志(Undo Log) 和ReadView(讀視圖)。三者協同工作,使得事務能夠訪問到符合其隔離級別的數據版本。
1. 數據的隱藏列
InnoDB存儲引擎會為表中的每一行數據添加三個隱藏列,用于記錄版本信息和事務相關數據:
- DB_TRX_ID:記錄最后一次修改該行數據的事務ID(6字節)。每次事務對行數據執行
INSERT
/UPDATE
/DELETE
操作時,都會將當前事務的ID寫入該列。 - DB_ROLL_PTR:回滾指針(7字節),指向該行數據的回滾日志(Undo Log),通過該指針可以找到數據的上一個版本。
- DB_ROW_ID:行ID(6字節),當表沒有主鍵或唯一索引時,InnoDB會用該列生成聚簇索引,確保每行數據有唯一標識。
2. 回滾日志(Undo Log)
回滾日志是MVCC實現多版本的基礎,用于保存數據被修改前的舊版本。
- 作用:
- 當事務需要回滾時,通過Undo Log恢復數據到修改前的狀態(支持事務的原子性);
- 為MVCC提供數據的歷史版本,供其他事務讀取(支持并發讀寫)。
- 生成時機:
當事務執行INSERT
/UPDATE
/DELETE
時,InnoDB會先將數據的舊版本寫入Undo Log,再修改實際數據。INSERT
:Undo Log記錄新插入的行信息,事務回滾時直接刪除該行;UPDATE
/DELETE
:Undo Log記錄修改前的行數據,事務回滾時通過回滾指針恢復舊版本。
- 版本鏈:
多次修改同一行數據時,Undo Log會形成一條“版本鏈”:每次修改后,新數據的DB_ROLL_PTR
指向舊版本的Undo Log,舊版本的DB_ROLL_PTR
再指向更早的版本,直至最初版本。
回滾日志為什么在update和delete時不會被立即刪除,而insert之后可以立即刪除?
update和delete之后還會被mvcc或者是快照讀用到**,這里舉個mvcc還需要回滾日志的例子**
MVCC(多版本并發控制)通過回滾日志來實現數據多版本管理,以解決并發事務中的讀 - 寫沖突等問題,在可重復讀隔離級別下體現得較為明顯。以下是一個基于MySQL的InnoDB存儲引擎的例子:
- 假設當前有三個事務,事務ID分別為100、200、300。
- 事務200先執行了一條SQL語句:
UPDATE user SET name = '小王3號' WHERE id = 1
,此時數據庫會將修改前的記錄相關信息寫入回滾日志,然后修改實際數據,并將數據的trx_id
更新為200。 - 接著事務200提交。
- 之后事務100開始執行查詢語句,此時會生成一個ReadView,視圖數組為
(100, 300)
,min_id
為100,max_id
為300。 - 然后事務300執行了一條SQL語句:
UPDATE user SET name = '小王4號' WHERE id = 1
,數據庫同樣會將修改前的記錄(即name = '小王3號'
)寫入回滾日志,再修改實際數據,將trx_id
更新為300。 - 此時事務100再次執行查詢語句,根據MVCC的規則:
- 若
row的trx_id
落于min_id
和max_id
之間,且不在視圖數組中,說明這個版本是已經提交的事務生成的,是可見的。 - 事務300的
trx_id
為300,在min_id
和max_id
之間,但在視圖數組中,所以其修改后的結果對事務100不可見。 - 事務200的
trx_id
為200,也在min_id
和max_id
之間,不在視圖數組中,所以事務100會根據回滾日志找到事務200修改前的記錄,查詢結果為name = '小王3號'
。
- 若
通過這個例子可以看到,MVCC利用回滾日志構建數據的舊版本,配合ReadView機制,讓事務100在事務300已修改數據并提交的情況下,仍然能查詢到符合可重復讀規則的結果,體現了回滾日志在MVCC中的重要作用。
3. ReadView(讀視圖)
ReadView是事務在讀取數據時生成的一個“快照”,用于判斷當前事務能看到哪些版本的數據。它本質上是一組用于過濾數據版本的規則,包含四個核心參數:
- m_ids:當前活躍(未提交)的事務ID列表。
- min_trx_id:活躍事務中最小的事務ID。
- max_trx_id:系統為下一個事務分配的ID(即當前最大事務ID+1)。
- creator_trx_id:生成該ReadView的事務ID。
4. 版本可見性判斷規則
事務讀取數據時,會根據ReadView的參數,對數據的DB_TRX_ID
(最后修改事務ID)進行判斷,決定是否可見:
- 若
DB_TRX_ID == creator_trx_id
:數據是當前事務自己修改的,可見。 - 若
DB_TRX_ID < min_trx_id
:修改該數據的事務在當前事務啟動前已提交,可見。 - 若
DB_TRX_ID > max_trx_id
:修改該數據的事務在當前事務啟動后才開始,不可見(需通過回滾指針找更早版本)。 - 若
min_trx_id ≤ DB_TRX_ID ≤ max_trx_id
:- 若
DB_TRX_ID
在m_ids
中(事務仍活躍):不可見(需找更早版本); - 若
DB_TRX_ID
不在m_ids
中(事務已提交):可見。
- 若
如果當前版本不可見,事務會通過DB_ROLL_PTR
沿著Undo Log的版本鏈向上查找,直到找到符合規則的可見版本。
三、不同隔離級別下的MVCC行為
MVCC的具體表現與事務隔離級別相關,主要差異在于ReadView的生成時機:
- 讀已提交(Read Committed):
每次執行SELECT
時都會重新生成ReadView。因此,事務在兩次查詢之間若有其他事務提交,可能會看到新提交的數據(“不可重復讀”)。 - 可重復讀(Repeatable Read):
僅在事務第一次執行SELECT
時生成ReadView,之后的查詢復用該ReadView。因此,事務在整個生命周期中看到的數據版本是一致的(“可重復讀”)。
四、MVCC的優勢
- 讀寫不沖突:讀操作通過訪問舊版本數據,無需等待寫操作釋放鎖;寫操作僅鎖定當前版本,不影響讀操作。
- 隔離級別靈活:通過ReadView的生成時機和版本判斷規則,適配不同隔離級別(讀已提交、可重復讀)。
- 支持事務回滾:結合Undo Log,確保事務失敗時數據可恢復(原子性)。
總結
MVCC通過“隱藏列記錄版本”、“Undo Log維護歷史版本鏈”、“ReadView過濾可見版本”三者的配合,實現了多版本數據的并發訪問。它既避免了傳統鎖機制的性能瓶頸,又保證了事務的隔離性,是現代數據庫高效處理并發的核心技術之一。