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




































