面試官:已經有鎖了,MySQL為什么還要引入MVCC?
今天我們來聊一個MySQL面試中的高頻考點,也是每一位后端開發者都應該深度掌握的核心知識——MVCC協議。
MVCC,全稱是多版本并發控制(Multi-Version Concurrency Control),它是MySQL InnoDB存儲引擎用以實現高效并發訪問的基石。在面試中,這幾乎是一個無法繞開的話題,透過它,就能通向對事務、隔離級別、鎖機制等一系列更深層次知識的探討。因此,徹底搞懂MVCC,能讓你在面試中游刃有余,從容地展現自己的技術深度。
在接下來的內容里,我將帶你從MVCC的底層原理出發,不僅讓你理解它“是什么”,更讓你明白它“為什么”如此設計,以及如何在面試中將這些知識轉化為你的優勢。
在正式開始之前,我們先來思考一個最根本的問題:在已經有鎖機制的情況下,InnoDB為什么還需要費盡心機地引入MVCC呢?
1. MVCC有什么用
你可能已經對數據庫的鎖機制有所了解,知道它是保障數據一致性、進行并發控制的基礎工具。那么,既然已經有了鎖,為什么InnoDB還要多此一舉引入MVCC呢?直接用讀寫鎖把所有并發訪問都管起來,不是更簡單直接嗎?
答案是:單純依賴鎖,性能實在太差了。
在一個純粹的鎖模型中,為了保證絕對的數據一致性,操作之間的互斥是不可避免的。寫操作會阻塞其他寫操作,這理所當然。但更致命的是,寫操作同樣會阻塞讀操作。即使用了所謂的“讀寫鎖”(允許多個讀操作并發,但讀寫、寫寫互斥),讀與寫之間的沖突依然存在。
數據庫系統與常規的應用程序有一個顯著的區別:它承載著高頻的讀寫請求,尤其是讀取操作,其性能表現至關重要。想象一下這個業務場景:一個核心業務線程正在執行UPDATE操作修改某一行關鍵數據,哪怕這個操作只需要幾十毫秒,但在這期間,所有希望讀取這行數據的SELECT請求都被迫掛起等待。在高并發系統中,這種阻塞會迅速累積,導致系統吞吐量急劇下降,用戶響應時間大幅延長。這樣的性能表現,在真實的線上環境中是絕對無法接受的。
圖片
顯然,為了掙脫這種性能枷鎖,數據庫必須尋求一種更優的解決方案,實現一種“讀寫并發”的理想狀態:當我在修改數據時,你依然可以無阻塞地讀取數據。 這就是MVCC誕生的核心驅動力。它通過一種巧妙的方式,讓讀和寫在大多數時候可以各行其道,互不干擾。
理解了MVCC存在的價值后,我們才能更好地去探究它的實現細節。而要理解MVCC,就必須先掌握一個與之緊密相連的前置概念——事務的隔離級別。
2. 事務的隔離級別
數據庫的隔離級別,本質上是定義的一套“游戲規則”,它用于規范在并發環境下,一個事務的修改對其他事務的可見性程度。換句話說,它界定了事務之間“互相了解”的邊界。ANSI/ISO SQL標準定義了四種隔離級別:
- 讀未提交(Read Uncommitted)
這是最低的隔離級別。在此級別下,一個事務可以看到另一個事務尚未提交的修改,因此風險極高,可能會導致“臟讀”。
圖片
- 讀已提交(Read Committed, RC)
這是大多數主流數據庫(如Oracle、PostgreSQL)默認的隔離級別。一個事務只能看到其他事務已經提交的修改。這確保了你讀到的數據至少是“真實存在”過的。但問題在于,在一個事務的執行過程中,如果其他事務提交了新的修改,本事務后續的查詢是能夠看到這些新變更的,從而導致“不可重復讀”。
圖片
- 可重復讀(Repeatable Read, RR)
這是MySQL InnoDB存儲引擎的默認隔離級別。它保證了在一個事務內部,無論你對同一份數據讀取多少次,得到的結果始終是一致的。這意味著,一旦事務開始,它就仿佛進入了一個“時間凝固”的快照中,即使在此期間有其他事務提交了修改,當前事務也“視而不見”。

- 串行化(Serializable)
這是最高的隔離級別。它通過對所有讀寫操作都加鎖的方式,強制所有事務串行執行,一個接一個地排隊處理。這能夠完全避免所有的并發問題,但代價是并發性能急劇下降,幾乎回到了單線程時代。
從上到下,隔離性越來越強,數據一致性保障越好,但并發性能也隨之下降。因此,選擇合適的隔離級別,是在業務需求和系統性能之間進行權衡的藝術。
與隔離級別相伴而生的,是三個經典的并發讀問題:
- 臟讀(Dirty Read):指讀到了其他事務還未提交的數據。這些數據因為隨時可能被回滾而消失,所以被稱為“臟”數據,是極不穩定的。
- 不可重復讀(Non-Repeatable Read):指在同一個事務中,對同一行數據前后兩次讀取,得到的結果不一致。其關注點在于數據內容的變更。
- 幻讀(Phantom Read):指在一個事務的執行過程中,另一個事務插入了新的數據行并提交,導致第一個事務在后續的查詢中,讀到了之前不存在的“幻影”行。其關注點在于數據行數的增減。

我們可以用一個表格來清晰地展示隔離級別與這三種讀問題的關系:
隔離級別 | 臟讀 | 不可重復讀 | 幻讀 |
讀未提交 | 可能 | 可能 | 可能 |
讀已提交 | 不可能 | 可能 | 可能 |
可重復讀 | 不可能 | 不可能 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 |
這里需要特別強調一點:根據SQL標準,可重復讀隔離級別是無法完全避免幻讀的。但是,MySQL的InnoDB引擎通過引入**臨鍵鎖(Next-Key Lock)**這一強大的鎖定機制,在RR級別下巧妙地解決了幻讀問題。在面試中提及這一點,并能解釋其原理,無疑會是一個重要的加分項。
此外,還有兩個概念需要精確區分:快照讀(Snapshot Read)和當前讀(Current Read)。簡單理解,快照讀(如普通的SELECT)讀取的是MVCC機制提供的歷史版本數據,它無須加鎖,速度很快。而當前讀(如SELECT ... FOR UPDATE、UPDATE、DELETE)讀取的是數據庫中最新的、已提交的版本,并且會對讀取的記錄加鎖,以保證數據的一致性。在MySQL的可重復讀隔離級別下,普通的SELECT語句執行的就是快照讀。
3. 版本鏈
為了實現MVCC,InnoDB為表中的每一行數據都額外增加了兩個隱藏的系統字段:trx_id和roll_ptr。
trx_id(Transaction ID):事務ID。MVCC中的“V”(Version)指的就是由不同事務ID創造的數據版本。每當一個事務開始時,它會獲得一個唯一的、單調遞增的事務ID。當這個事務修改某行數據時,該行的trx_id就會被更新為當前事務的ID。roll_ptr(Rollback Pointer):回滾指針。它是一個指向undo log中上一版本記錄的指針。InnoDB正是通過這個指針,將一行數據的多個歷史版本像鏈表一樣串聯起來,形成一個“版本鏈”。
實際上,InnoDB還有一個隱藏的row_id列,在沒有顯式定義主鍵時,它會作為內部主鍵。但它與MVCC的并發控制邏輯關系不大,我們在此不做過多關注。
下面,我們通過一個具體的例子來直觀地理解版本鏈是如何構建的。
假設我們向表中插入一條新數據{id: 1, x: 130},執行該操作的事務ID為100。此時,這行數據的最新版本狀態如下:

現在,一個新的事務A(ID為101)啟動,并將x的值修改為150。這時,數據庫并不會直接覆蓋原始數據,而是會執行以下操作:
- 將原始行數據
{id: 1, x: 130, trx_id: 100}完整地拷貝到undo log中。 - 在原始數據行上進行修改,將
x更新為150,并將trx_id更新為當前事務的ID,即101。 - 將這行新數據的
roll_ptr指向剛剛在undo log中創建的舊版本記錄。
修改后,數據行的最新狀態變為:
圖片
接著,又來了一個事務B(ID為102),它將x的值從150修改為200。同樣地,數據庫會重復上述過程,將trx_id為101的版本拷貝到undo log,然后更新數據行的最新版本,并將roll_ptr指向trx_id為101的版本。
圖片
這樣一來,通過roll_ptr,一行數據的多個歷史版本就被從新到舊地串聯成了一條鏈表。這條鏈就是大名鼎鼎的版本鏈。
現在問題來了:如果一個新的事務C想要讀取x的值,它面對的是一條長長的版本鏈,它應該讀取哪個版本的數據呢?這就引出了MVCC的另一個核心裁決機制:Read View。
3.1 Read View(讀視圖)
你可以將Read View(讀視圖)理解為一套在特定時刻生成的“可見性規則快照”。當一個事務需要進行快照讀時,數據庫會依據這個Read View來掃描版本鏈,判斷哪個版本是對當前事務可見的。
Read View主要在“讀已提交(RC)”和“可重復讀(RR)”這兩個隔離級別下工作。它們的核心區別,就在于生成Read View的時機。
- 讀已提交(RC):事務中每一次
SELECT查詢開始時,都會重新生成一個新的Read View。 - 可重復讀(RR):僅在事務第一次
SELECT查詢時,生成一個Read View,并在此后的整個事務期間都復用這個Read View。
這個區別可以用一個形象的比喻來描述:RC隔離級別像一個每次約會都換新對象的“花心大少”,他眼中的世界總是在變化;而RR隔離級別則像一個從始至終只認初戀的“癡情種子”,無論外界如何變遷,他眼中的戀人永遠是最初的模樣。
3.2 Read View與讀已提交(RC)
在RC隔離級別下,每次查詢都會生成新的Read View,這意味著在事務執行過程中,可見性判斷的基準是動態變化的。
我們來看一個例子。假設當前數據庫中有三個事務修改過的歷史版本,狀態如下:
圖片
現在,一個新的事務A(trx_id為4)啟動了。當它第一次查詢x的值時,MySQL會創建一個Read View。此時,活躍(未提交)的事務ID集合是m_ids = {2, 3}。事務A會沿著版本鏈從最新版本開始查找,它會跳過trx_id為3和2的版本(因為它們的事務ID在活躍事務列表m_ids中,意味著它們是“未提交”或“并發”的事務),最終找到trx_id為1的版本。這個版本已經提交,所以事務A讀到的x的值是10。
圖片
緊接著,事務2提交了。然后事務A在同一個事務內再次查詢x。由于是RC隔離級別,MySQL會重新生成一個Read View。此時,活躍事務列表變成了m_ids = {3}。事務A再次沿著版本鏈查找,它會跳過trx_id為3的版本,但當它檢查到trx_id為2的版本時,發現2已經不在新的活躍列表m_ids中了(意味著事務2已提交),于是它讀取了這個版本的數據。因此,事務A這次讀到的x的值是40。這就是“不可重復讀”的由來。
圖片
3.3 Read View與可重復讀(RR)
在RR隔離級別下,Read View在事務第一次查詢時創建,并在整個事務期間保持不變。
我們用同樣的例子來說明。當事務A(ID為4)第一次查詢時,創建了一個Read View,其活躍事務列表為m_ids = {2, 3}。
圖片
此時,事務A查詢x的值,與RC級別下的第一次查詢一樣,它會忽略trx_id為2和3的未提交版本,最終讀到trx_id為1的版本,結果為x=10。
圖片
接下來,即使事務2提交了,當事務A再次查詢x的值時,它使用的仍然是事務開始時創建的那個舊的Read View。在這個舊的Read View中,m_ids依然是{2, 3}。所以,對于事務A來說,事務2看起來仍然是“未提交”的(因為事務2的ID存在于它持有的那個舊的Read View的m_ids列表中)。因此,它會再次忽略trx_id為2的版本,最終讀到的結果仍然是x=10。
圖片
這就是“可重復讀”的實現原理。無論其他事務如何提交,當前事務的“視界”在事務開始的那一刻就已經被固定下來了。
3.4 Read View小結
為了讓你更清晰地理解,我將上述過程總結為兩張圖。
讀已提交(RC)下的Read View變化:
圖片
可重復讀(RR)下的Read View固定:
圖片
實際上,一個完整的Read View除了m_ids(活躍事務ID列表)外,還包含其他幾個關鍵字段,共同構成了可見性判斷的完整邏輯:
m_up_limit_id:m_ids列表中的最小事務ID。任何trx_id小于此值的版本,都表示是“已提交”的,因此可見。m_low_limit_id:當前系統中下一個將被分配的事務ID。任何trx_id大于等于此值的版本,都表示是“未來”的事務,因此不可見。m_creator_trx_id:創建該Read View的事務自身的ID。自身的修改總是可見的。
可見性判斷的完整規則可以概括為:
圖片
對于一個版本鏈上的數據行,其trx_id會與Read View的這幾個字段進行比較,以確定其是否可見。不過,在面試中,你只需要記住核心邏輯——m_ids和Read View的創建時機——就足以理解MVCC的精髓了。
4. 面試實戰指南
掌握了前面的基礎知識,我們來看看在面試中如何將這些知識轉化為你的優勢。
首先,你必須清楚自己公司生產環境數據庫的隔離級別。如果不是默認的RR,那你一定要搞清楚為什么要做這樣的調整,這本身就是一個很好的實踐案例。
面試官可能會現場構造一個并發場景來考察你。我的建議是,對這類問題提前做好心理準備。如果一時反應不過來,不要慌張,可以禮貌地請面試官慢速復述一遍問題,甚至可以主動請求使用紙筆,將版本鏈和Read View的演變過程畫出來再分析,這不僅能幫助你理清思路,還能向面試官展示你扎實、嚴謹的分析能力。
4.1 基本思路
當面試官從鎖的問題過渡到MVCC,問“為什么有了鎖還需要MVCC”時,你的回答要突出關鍵詞:避免讀寫阻塞,實現讀寫并發。
“單純使用鎖機制,并發性能會很差。即使是讀寫鎖,讀和寫操作之間仍然是互斥的。數據庫作為高性能中間件,如果一個寫操作就導致所有讀操作被阻塞,這種性能損失是無法接受的。因此,InnoDB引擎引入了MVCC,其核心目的就是通過空間換時間的方式,實現讀寫操作的并發執行,極大地提升了數據庫的并發處理能力。”
更多時候,面試官會直接提問MVCC本身。這時,你可以按照“定義 -> 實現機制 -> 關聯隔離級別”的邏輯順序,簡明扼要地回答:
“MVCC是MySQL InnoDB引擎用于實現高并發訪問的一種協議。它的核心實現主要依賴于兩大組件:版本鏈(Version Chain)和讀視圖(Read View)。
首先,在InnoDB中,每一行數據都有兩個隱藏列:
trx_id(最后修改該行的事務ID)和roll_ptr(回滾指針)。通過回滾指針,InnoDB將一行數據的多個歷史版本在undo log中串聯起來,形成版本鏈。
其次,當一個事務發起快照讀時,MVCC會根據該事務的隔離級別(讀已提交或可重復讀)生成一個Read View。這個Read View定義了一套可見性規則,事務會用這個Read View去匹配版本鏈,從而找到對當前事務可見的那個特定版本的數據。”
這個回答非常簡潔,但覆蓋了所有關鍵點,并且為面試官的追問留下了引子。
4.2 亮點方案:推動隔離級別調整
這是一個可以充分展示你實踐經驗和思考深度的亮點。你可以描述你如何推動公司將數據庫隔離級別從默認的RR調整為RC。
你需要說清楚兩點:
- 為什么要把默認的RR降級為RC?
- 降級后,如果真的遇到需要RR特性的場景,該如何處理?
你可以這樣組織你的回答:
“在我之前參與的一個項目中,我發現我們數據庫普遍使用的是MySQL默認的‘可重復讀’(RR)隔離級別。但經過深入的業務場景分析后,我發現絕大多數事務并不需要‘可重復讀’的特性,比如一個事務內對同一數據的多次讀取幾乎不存在。
與此同時,使用RR級別卻帶來了一些實際問題。首先,RR級別由于臨鍵鎖的存在,比RC級別更容易在并發寫入時引發間隙鎖導致的死鎖。我們線上也確實遇到過因此產生的棘手死鎖問題。其次,從性能角度看,RC級別下,事務提交后會更快地釋放鎖,并且
undo log的保留鏈條通常更短,這都意味著RC級別能提供更好的并發性能。
基于這些考慮——業務不需要、存在死鎖風險、性能更優——我主導并推動了公司數據庫隔離級別的調整,將新業務的默認級別從RR降級為RC,從而提升了系統的整體性能和穩定性。”
此時,面試官很可能會追問:“這個方案很好,但調整之后,如果某個特殊業務確實需要可重復讀的特性,你怎么辦?”
“這是一個非常好的問題,我們在推進時也充分考慮了這一點。我們的解決方案是分層處理:
首先,我們會優先嘗試從業務邏輯層面進行改造。很多所謂的‘可重復讀’需求,其實是可以通過優化代碼來滿足的。例如,如果一個業務流程中需要多次使用同一份數據,我們完全可以在第一次讀取后將結果在應用層面緩存起來(比如放在一個變量里),供后續流程使用,這樣就自然避免了對數據庫的多次查詢。
至于幻讀,在絕大多數互聯網業務中,它通常不被視為一個嚴重的問題。原因有二:一是業務代碼很難區分讀到的新數據是幻讀,還是在事務開始前就已存在的數據。比如你在事務 A 里面讀到了一條數據,你判斷不出來它是在事務 A 開始之前就插入的,還是在事務 A 開始之后,事務 B 才插入并且提交的。
圖片
二是事務的提交通常意味著一筆業務邏輯的完結。如果事務A讀到了事務B新插入并已提交的數據,從業務角度看,可以認為事務B所代表的業務已經完成了,那么事務A讀到這個“新”結果也是合乎邏輯的。
當然,如果遇到非常極端、無法通過業務改造來解決的場景,我們還有最后的兜底方案:在代碼中為單個事務顯式指定隔離級別。我之前調整的是數據庫的全局默認隔離級別,但MySQL允許在Session級別,甚至是單個事務級別通過
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;來動態設置隔離級別。這樣既能讓絕大多數業務享受RC級別帶來的整體性能提升,又能以最小的代價靈活應對個別特殊需求,實現了全局最優和局部最優的統一。”
5. 小結
回到開篇:既然有鎖,為什么還要引入 MVCC?答案就在于 MVCC 用版本鏈 + ReadView把讀寫解耦了。在保證必要隔離的同時大幅提升并發讀取性能。
鎖解決的是誰能改的問題,而 MVCC 解決的是該讀誰的數據、在什么時候讀的問題。它以版本鏈和可見性規則,讓每個事務都能在自己的時間線里安全讀取數據,不必被鎖束縛。正因為有了 MVCC,MySQL 才真正實現了讀寫并行,在一致性與性能之間取得平衡。它不是為了取代鎖,而是彌補鎖的局限,讓數據庫在高并發的世界里依然保持秩序與速度——這正是 MVCC 存在的意義,也是事務并發控制的靈魂所在。



































