?MVCC(Multi-Version Concurrency Control,多版本并發控制)。是一個在數據庫管理系統中用于處理并發控制的核心技術。理解它對于深入掌握數據庫(尤其是 InnoDB、PostgreSQL 等)的工作原理至關重要。
1. 什么是 MVCC?
MVCC 的全稱是?多版本并發控制。
核心思想:在數據庫中,同一份數據可以保留多個歷史版本。當事務需要讀取數據時,它會根據一定的規則(比如事務的開始時間)看到一個特定的、一致性的“快照”(Snapshot),而不是直接讀取最新的、可能還未提交的數據。寫操作則會創建一個新版本的數據。
你可以把它想象成一個高效的版本控制系統(如 Git):
- 讀操作:就像?
git checkout
?到某個特定的 commit 版本,你看到的是那個時間點的完整項目狀態,即使之后有新的提交,你的視圖也不會變。 - 寫操作:就像創建一個新的?
commit
,它不會覆蓋舊的?commit
,而是在舊版本的基礎上生成一個新版本。
通過這種方式,讀操作和寫操作可以不再互相阻塞,從而極大地提高了數據庫的并發性能。
2. 為什么需要 MVCC?
在傳統的數據庫并發控制中,主要使用兩種機制:
鎖機制:
- 讀-寫沖突:當一個事務在讀取一行數據時,會給它加上共享鎖(S鎖)。另一個事務如果想修改這行數據,需要加排他鎖(X鎖),但 S 鎖和 X 鎖互斥,所以寫事務必須等待讀事務完成。反之亦然,寫事務會阻塞讀事務。
- 問題:并發度低。讀和寫操作串行化,性能很差。
基于時間戳的排序:
- 所有操作按時間戳排序執行,如果操作沖突,則回滾其中一個事務。
- 問題:事務沖突率高,回滾頻繁,性能同樣不理想。
MVCC 的出現就是為了解決這些問題,它提供了一種“樂觀”的并發控制方式:
- 讀寫不沖突:讀數據(快照讀)不會阻塞寫數據,寫數據也不會阻塞讀數據。這是 MVCC 最大的優勢。
- 非鎖定讀:大多數情況下,普通的?
SELECT
?查詢不需要加鎖,避免了鎖的開銷和死鎖的風險。 - 實現事務隔離:MVCC 是實現數據庫事務隔離級別(特別是?
READ COMMITTED
?和?REPEATABLE READ
)的基礎。
3. MVCC 是如何工作的?
MVCC 的實現依賴于三個關鍵組件:隱藏列、Undo Log?和?Read View。我們以最經典的?MySQL InnoDB 存儲引擎為例來講解。
a. 隱藏列
InnoDB 會為每一行數據額外添加三個隱藏的字段:
DB_TRX_ID
?(6字節):?最后修改該行的事務ID。記錄了最后一次對這行記錄進行?INSERT
?或?UPDATE
?的事務ID。每次事務修改一行,這個字段都會被更新。DB_ROLL_PTR
?(7字節):?回滾指針。它指向該行上一個版本的數據在 Undo Log 中的位置。通過這個指針,可以形成一個“版本鏈”,把一個數據行的所有歷史版本串聯起來。DB_ROW_ID
?(6字節):?隱藏的行ID。一個單調遞增的ID,當表沒有顯式主鍵時,InnoDB會用它來生成一個聚集索引。
版本鏈示例:
假設一行數據被事務 10、事務 20 依次修改。
- 初始狀態:事務 10 插入一行數據。
DB_TRX_ID = 10
DB_ROLL_PTR = null
?(因為是第一個版本)
- 事務 20 修改:事務 20 更新了這行數據。
- InnoDB?不會直接覆蓋舊數據,而是:
- 將舊版本的數據(
DB_TRX_ID=10
?的版本)復制到 Undo Log 中。 - 在原位置創建一個新版本的數據行。
- 更新新版本的字段:
DB_TRX_ID = 20
。 - 更新新版本的?
DB_ROLL_PTR
,讓它指向 Undo Log 中舊版本的位置。
- 將舊版本的數據(
- 現在,通過新版本的?
DB_ROLL_PTR
,我們可以找到舊版本,形成一條?版本鏈
:最新版本(20) -> 舊版本(10)
。
- InnoDB?不會直接覆蓋舊數據,而是:
b. Undo Log
Undo Log 主要有兩個作用:
- 事務回滾:當一個事務需要回滾時,可以利用 Undo Log 中記錄的舊版本數據,將數據恢復到修改之前的狀態。
- 構建版本鏈:如上所述,它存儲了數據行的歷史版本,是 MVCC 實現多版本的關鍵。當需要讀取某個歷史版本時,就可以從這里獲取。
c. Read View(讀視圖)
Read View 是事務在執行快照讀(普通的?SELECT
)時,動態生成的一個“可見性判斷”標準。它決定了當前事務能看到版本鏈上的哪個版本。
Read View 主要包含以下幾個重要屬性:
creator_trx_id
: 創建該 Read View 的事務的 ID。trx_ids
: 創建 Read View 時,當前系統中所有活躍的(未提交的)讀寫事務的 ID 列表。up_limit_id
:?trx_ids
?列表中事務 ID 的最小值。如果版本鏈上某個版本的?DB_TRX_ID
?小于?up_limit_id
,則表示這個版本在創建 Read View 之前已經提交,所以對當前事務是可見的。low_limit_id
: 創建 Read View 時,系統應該分配給下一個事務的 ID。如果版本鏈上某個版本的?DB_TRX_ID
?大于或等于?low_limit_id
,則表示這個版本是在創建 Read View 之后才開啟的事務中修改的,所以對當前事務是不可見的。
4. MVCC 如何解決并發問題?
現在,我們把這三個組件結合起來,看看一個?SELECT
?語句是如何利用 MVCC 找到它應該看到的數據版本的。我們以?InnoDB 的?REPEATABLE READ
(可重復讀)隔離級別為例。
核心判斷流程:
當一個事務(假設 ID 為?T1
)執行?SELECT
?時,它會獲取一個 Read View。然后,它會從版本鏈的最新版本開始,逐個版本地應用以下規則,直到找到一個可見的版本:
檢查?
DB_TRX_ID
?是否是自己創建的?- 如果?
DB_TRX_ID == creator_trx_id
,說明這行數據是本事務自己修改的,可見。
- 如果?
檢查?
DB_TRX_ID
?是否小于?up_limit_id
?- 如果?
DB_TRX_ID < up_limit_id
,說明修改這個版本的事務在當前事務開始前就已經提交了,可見。
- 如果?
檢查?
DB_TRX_ID
?是否大于或等于?low_limit_id
?- 如果?
DB_TRX_ID >= low_limit_id
,說明修改這個版本的事務是在當前事務開始之后才啟動的,不可見。需要根據?DB_ROLL_PTR
?去 Undo Log 中查找上一個版本,然后重復整個判斷流程。
- 如果?
檢查?
DB_TRX_ID
?是否在?trx_ids
?列表中?- 如果?
up_limit_id <= DB_TRX_ID < low_limit_id
,則需要判斷?DB_TRX_ID
?是否在活躍事務列表?trx_ids
?中。 - 如果在:說明修改這個版本的事務在當前事務創建 Read View 時還未提交,不可見。需要去 Undo Log 中找上一個版本。
- 如果不在:說明修改這個版本的事務在當前事務創建 Read View 時已經提交了,可見。
- 如果?
最終:如果遍歷完整個版本鏈都找不到可見的版本,說明這行數據對當前事務是不可見的(比如被其他事務刪除了)。
REPEATABLE READ
?vs?READ COMMITTED
?的關鍵區別
REPEATABLE READ
?(可重復讀):- 事務中第一次執行?
SELECT
?時,會創建一個 Read View,之后該事務內的所有?SELECT
?都復用這個 Read View。 - 效果:確保了在同一個事務中,多次讀取同一數據的結果是一致的,因為判斷可見性的標準(Read View)從未改變。這就是“可重復讀”的由來。
- 事務中第一次執行?
READ COMMITTED
?(讀已提交):- 事務中每次執行?
SELECT
?時,都會重新創建一個新的 Read View。 - 效果:每次讀取都能看到其他已提交事務所做的最新修改。因為每次的 Read View 都是最新的,
up_limit_id
?和?trx_ids
?都會更新,所以之前不可見的版本可能就變得可見了。
- 事務中每次執行?
5. MVCC 的優缺點
優點
- 高并發性:讀寫操作不阻塞,極大地提高了數據庫的并發讀寫性能。
- 非鎖定讀:避免了讀操作加鎖帶來的開銷和死鎖風險。
- 實現一致性讀:為不同隔離級別提供了基礎,保證了事務的隔離性。
缺點
- 存儲空間開銷:需要維護多個版本的數據,Undo Log 會占用額外的存儲空間。對于長事務或更新頻繁的表,Undo Log 可能會變得非常大。
- 管理開銷:需要額外的邏輯來管理版本鏈、創建和判斷 Read View,增加了數據庫的復雜性。
- 行版本清理:需要后臺線程(如 InnoDB 的 Purge 線程)定期清理已經不再需要的舊版本數據(即沒有事務再需要訪問它們),否則 Undo Log 會無限增長。這個清理過程本身也消耗資源。
- 并非萬能:MVCC 主要解決的是?
SELECT
?的并發問題。對于?UPDATE
、DELETE
?之間的沖突,仍然需要使用鎖(比如行鎖、間隙鎖、Next-Key Locks)來保證數據的一致性和防止幻讀。
6. MVCC 與隔離級別的關系
隔離級別 | MVCC 如何工作 | 能解決的問題 |
---|---|---|
READ UNCOMMITTED?(讀未提交) | 基本不使用 MVCC。直接讀取最新的數據,即使它未提交。 | 無 |
READ COMMITTED?(讀已提交) | 每次?SELECT ?都創建新的 Read View。只能讀到已提交的數據。 | 解決臟讀 |
REPEATABLE READ?(可重復讀) | 事務中第一次?SELECT ?創建 Read View,后續復用。保證同一事務內多次讀取結果一致。 | 解決臟讀、不可重復讀 (在 InnoDB 中,結合 Next-Key Locks 還能解決幻讀) |
SERIALIZABLE?(可串行化) | 基本不使用 MVCC 的快照讀。所有?SELECT ?語句都會隱式地轉換為?SELECT ... LOCK IN SHARE MODE ,即加共享鎖。讀寫操作都互相阻塞。 | 解決所有并發問題(臟讀、不可重復讀、幻讀) |
7. 總結
MVCC 是一種優雅而強大的并發控制技術,其精髓在于“用空間換時間,用版本換鎖”。
- 核心:通過為數據維護多個版本,讓讀操作訪問歷史快照,寫操作創建新版本。
- 關鍵組件:隱藏列(
DB_TRX_ID
,?DB_ROLL_PTR
)構建版本鏈,Undo Log 存儲歷史版本,Read View 定義可見性規則。 - 目的:實現讀寫不阻塞,提高并發性能,并作為實現數據庫事務隔離級別的基礎。
- 應用:廣泛應用于現代主流數據庫,如?MySQL InnoDB、PostgreSQL、Oracle?等。