CMU15-445 數據庫系統播客:深入理解多版本并發控制 (MVCC) - 現代數據庫的并發基石
構建任何高并發應用時,數據庫的性能和穩定性都是我們關注的重中之重。而談到數據庫,一個我們無法繞開的概念就是 并發控制 。今天,我們將深入探討現代關系型數據庫中最主流的并發控制思想—— 多版本并發控制(Multi-Version Concurrency Control, MVCC) 。
無論是 PostgreSQL, MySQL (InnoDB), Oracle, 還是 SQL Server(在特定隔離級別下),MVCC 都是它們處理高并發讀寫請求、保證數據一致性的核心機制。
核心思想:MVCC 是什么?
許多初學者可能會誤解,認為 MVCC 是一種與兩階段鎖(2PL)或樂觀并發控制(OCC)并列的并發控制“協議”。但一個更精確的定義是: MVCC 是一種實現并發控制的系統架構(System Architecture) 。
它的核心理念極其優雅: 通過保留數據的多個歷史版本,來實現讀寫操作的并行不悖 。
想象一下傳統的鎖機制:當一個事務正在寫入某行數據時,這行數據就會被“鎖”住。此時,其他任何想要讀取這行數據的事務都必須等待,直到寫事務提交或回滾。同樣,一個事務在讀取數據時,也可能阻塞其他事務的寫入。這種讀寫相互阻塞的模式,在并發量高的場景下會嚴重影響系統吞吐量。
MVCC 則徹底改變了這一游戲規則。它的核心承諾是:
寫操作不阻塞讀操作,讀操作也不阻塞寫操作。
這是如何實現的呢?當一個事務需要讀取數據時,系統不會直接返回“最新”的數據,而是為其提供一個 “一致性快照(Consistent Snapshot)” 。這個快照代表了該事務啟動時,整個數據庫的某個一致性狀態。如此一來,即使其他事務正在修改數據,當前事務讀取的也只是它自己快照中的、未被修改的舊版本,完全不受干擾。
然而,需要明確的是,MVCC 自身并不解決 寫-寫沖突(Write-Write Conflicts) 。當兩個事務嘗試修改 同一個 數據對象時,系統仍然需要一個仲裁機制來決定誰勝誰負。這時,MVCC 架構就需要與一個傳統的并發控制協議(如時間戳排序、樂觀鎖或兩階段鎖)相結合,來處理這類沖突。
歷史回眸:一個經久不衰的理念
MVCC 的思想并非憑空出現。它的理論雛形最早可以追溯到 1978 年麻省理工學院(MIT)的一篇博士論文 。但真正將其工程化并推向市場的,是 20 世紀 80 年代早期的 DEC 公司 。
DEC 公司開發的兩款數據庫產品—— Rdb/VMS 和 InterBase ,是商業上最早成功實現 MVCC 的系統。這背后的關鍵人物是 Jim Starkey ,他不僅是 MVCC 的早期實踐者,還被認為是 BLOBs(二進制大對象)和數據庫觸發器的發明人。
這段歷史還有一些有趣的后續:
- DEC 的 Rdb/VMS 最終被 Oracle 收購,成為了 Oracle Rdb。
- InterBase 在幾經易手后被開源,演變成了今天我們所熟知的 Firebird 數據庫。
一個有趣的花絮是:Mozilla 最初想將瀏覽器命名為 Phoenix,因版權沖突改為 Firebird,但再次與 Firebird 數據庫重名,最終才定名為我們熟悉的 Firefox。
MVCC 的兩大殺手級優勢
為什么 MVCC 能夠經受住時間的考驗,成為現代數據庫的標配?因為它帶來了兩個無與倫比的優勢:
極致高效的只讀事務
在 MVCC 架構下,只讀事務的執行速度快得驚人。當一個事務被聲明為只讀(Read-Only)時,數據庫系統知道它絕不會修改數據。因此,系統無需為其獲取任何鎖,也無需追蹤其讀寫集。它要做的僅僅是讀取其事務開始時那個“一致性快照”。這幾乎是零成本的并發,極大地提升了讀多寫少場景下的系統性能。
強大的“時間旅行”查詢能力
由于 MVCC 天然地保存了數據的歷史版本,它為實現“時間旅行查詢(Time-Travel Queries)”提供了可能。這意味著你可以向數據庫提出這樣的問題:
- “三天前,我們的用戶表是什么狀態?”
- “上個季度末,公司的總銷售額是多少?”
這個概念最早由 Postgres 在 20 世紀 80 年代提出。但遺憾的是,除了特定領域的數據庫外,大多數通用數據庫系統并沒有完全開放這個功能。主要原因在于成本:支持任意時間點的查詢意味著 必須永久保留所有歷史版本 ,這將導致存儲空間的無限膨脹。
盡管如此,在某些對數據審計和歷史追溯有強需求的領域(如 金融行業 ),時間旅行查詢的價值巨大。法規可能要求金融機構保留長達數年的交易記錄,MVCC 使得在這海量歷史數據中進行查詢變得異常高效。
深入實現:MVCC 的四大核心設計決策
實現一個健壯高效的 MVCC 系統,遠比聽起來復雜。數據庫開發者必須在四個關鍵領域做出精心的設計和權衡。
并發控制協議 (Concurrency Control Protocol)
如前所述,MVCC 架構需要一個“伙伴”協議來處理寫-寫沖突。不同的數據庫選擇了不同的方案:
- 時間戳排序 (Timestamp Ordering, T/O) : 每個事務獲取一個時間戳,系統根據時間戳的先后順序來決定事務的執行次序。
- 樂觀并發控制 (Optimistic Concurrency Control, OCC) : 事務執行期間不做任何檢查,直到提交時才檢查是否存在沖突。如果沖突,則回滾。
- 兩階段鎖 (Two-Phase Locking, 2PL) : 仍然使用鎖來解決寫-寫沖突,但讀操作不受影響。
這個協議的選擇,直接決定了數據庫在不同沖突場景下的行為和性能。
版本存儲 (Version Storage)
這是 MVCC 實現中最核心的部分:如何組織和存儲一個邏輯數據的多個物理版本?通常,系統會為每個 邏輯元組(Logical Tuple) 維護一個 版本鏈(Version Chain) ,而索引指向這個鏈的“頭部”。
主要有三種主流方案:
僅追加存儲 (Append-Only Storage)
這是最直觀的方式,也是 PostgreSQL 采用的策略。當一個元組被更新時,系統不會在原地修改它,而是:
- 將舊版本的完整內容復制一份,形成一個新的物理元組。
- 在這個新的物理元組上執行修改。
- 將版本鏈的指針指向這個新版本。
這種方式又引申出版本鏈的組織順序問題:
- 從舊到新 (Oldest-to-Newest, O2N) : 新版本追加在鏈表的末尾。優點是追加操作簡單;缺點是查找最新版本時可能需要遍歷整個鏈。
- 從新到舊 (Newest-to-Oldest, N2O) : 新版本放在鏈表的頭部。優點是查找最新版本非常快(O(1));缺點是每次創建新版本,所有指向舊頭部的索引都必須更新為指向新頭部,更新開銷較大。
權衡分析 :僅追加存儲的 讀取舊版本性能極好 ,因為舊元組是完整獨立的,無需任何計算。但其 寫入開銷較大 ,即使只修改一個字段,也需要復制整個元組。
時間旅行存儲 (Time-Travel Storage)
這種方案將主表和歷史表分開。主表上永遠只保存最新版本的數據。當數據被更新時,舊版本被復制到一個獨立的 “時間旅行表” 中。這種方式邏輯清晰,但可能增加數據管理的復雜性。
Delta 存儲 (Delta Storage)
這是 MySQL (InnoDB) 和 Oracle 采用的策略。其思想類似于 Git 的 diff:不存儲完整的舊版本,而 只記錄被修改字段的“增量(Delta)” 。
當一個元組被更新時,系統會將修改前的“舊值”存放到一個獨立的 Delta 存儲區(在 InnoDB 中稱為 Rollback Segment),然后在主表上進行原地更新。版本鏈實際上是通過指針串聯起來的 Delta 記錄。
權衡分析 :Delta 存儲的 寫入性能通常更高 ,因為只需復制少量修改的字段,而非整個元組。但其 讀取舊版本的開銷更大 ,因為需要從最新版本開始,通過“重放(Replay)”一系列的 Delta 記錄來逐步回溯,才能重建出目標歷史版本。
性能對決的根源 :正是由于版本存儲策略的根本不同,我們經常觀察到:在讀密集型,特別是需要讀取歷史數據的分析場景下, PostgreSQL 可能表現更優;而在寫密集型,特別是更新操作頻繁的 OLTP 場景下, MySQL (InnoDB) 可能更具優勢。
垃圾回收 (Garbage Collection)
隨著系統運行,會產生大量不再需要的舊版本(例如,所有活躍事務都無法再看到它們,或者由已中止事務創建的版本)。垃圾回收機制(常被稱為 VACUUM)負責清理這些“垃圾”以回收磁盤空間。
何時可以回收? 一個版本可以被安全回收的條件是:當前系統中沒有任何一個活躍事務的“快照時間戳”會落在該版本的生命周期內。
如何高效回收?
- 后臺清掃 (Background Vacuuming) : 由一個或多個專用后臺線程定期掃描表,查找并清理無效版本。為了避免全表掃描的巨大開銷,系統通常會維護一個 “臟頁位圖(Dirty Page Bitmap)” ,只檢查那些被修改過的數據頁。這是 PostgreSQL 的主要方式。
- 協作式清理 (Cooperative Cleaning) : 工作線程在執行查詢、遍歷版本鏈的過程中,“順手”清理掉它們遇到的無效舊版本。這種方式非常巧妙,但它 僅適用于從舊到新(O2N)的版本鏈 ,因為只有在這種順序下,查找新版本才會自然地經過舊版本。
索引管理 (Index Management)
在 MVCC 中,索引不僅要能找到數據,還要能找到 正確版本 的數據。
主鍵索引 (Primary Key Indexes) : 通常比較直接,索引條目直接指向版本鏈的頭部。如果主鍵本身被更新,系統通常將其處理為一次 DELETE + 一次 INSERT。
二級索引 (Secondary Indexes) : 處理起來要復雜得多,直接影響數據庫的性能特征。
- 物理指針 (Physical Pointers) : 二級索引的葉子節點直接存儲元組的物理地址(如:頁ID + 頁內偏移量)。這是 PostgreSQL 的典型實現。
a.優點 : 查詢速度快。通過二級索引能一次性定位到數據,無需額外查找。
b.缺點 : 更新開銷大。在 PostgreSQL 的僅追加模型中,一次 UPDATE 會導致元組產生新的物理位置。這意味著, 所有 指向該元組的二級索引(可能有很多個)都必須被更新,這在寫密集型負載下會成為巨大的性能瓶頸。
- 邏輯指針 (Logical Pointers) : 二級索引的葉子節點存儲的是一個不變的邏輯標識符,通常是 主鍵的值 。這是 MySQL (InnoDB) 的實現方式。
a.優點 : 更新開銷小。當元組更新(即使物理位置改變)時,只要主鍵不變,所有二級索引都 無需改動 。這極大地提升了寫入性能。
b.缺點 : 查詢需要“回表”。通過二級索引查找時,只能先找到主鍵值,然后必須再回到主鍵索引(聚簇索引)中進行第二次查找,才能定位到最終的數據。這增加了一次額外的索引查找開銷。
總結
MVCC 不僅僅是一個技術術語,它是一種精妙的設計哲學,是現代數據庫能夠在高并發世界中保持優雅和高效的關鍵。它通過多版本的“時空”換取了讀寫并發的“自由”,但其實現背后充滿了深刻的權衡。
從版本存儲(Append-Only vs. Delta)到索引管理(物理指針 vs. 邏輯指針),每一個設計決策都直接影響了數據庫(如 PostgreSQL 和 MySQL)在不同應用場景下的性能表現。理解這些內在機制,不僅能幫助我們更好地選擇和使用數據庫,更能讓我們在進行系統設計和性能優化時,做到胸有成竹,游刃有余。





































