深入解析:通用:MySQL-深入理解MySQL中的MVCC:原理、實現(xiàn)與實戰(zhàn)價值
深入理解MySQL中的MVCC:原理、實現(xiàn)與實戰(zhàn)價值
支撐高并發(fā)讀寫的核心技術(shù)。它解決了傳統(tǒng)鎖機制中“讀阻塞寫、寫阻塞讀”的痛點,讓數(shù)據(jù)庫在保證數(shù)據(jù)一致性的同時,能高效處理大量并發(fā)請求。本文將從定義、核心原理、實現(xiàn)組件、工作流程到實戰(zhàn)應(yīng)用,全方位拆解MVCC,幫你搞懂它在實際工作中的作用。就是在MySQL的InnoDB存儲引擎中,MVCC(Multi-Version Concurrency Control,多版本并發(fā)控制)
什么?—— 從核心定義到解決的痛點就是一、MVCC
1.1 核心定義
MVCC本質(zhì)是一種“多版本數(shù)據(jù)管理策略”:InnoDB會為數(shù)據(jù)庫中的每條數(shù)據(jù)記錄維護多個版本,當事務(wù)對數(shù)據(jù)進行修改時,不會直接覆蓋原數(shù)據(jù),而是生成一個新的數(shù)據(jù)版本;同時,讀取數(shù)據(jù)時,會根據(jù)事務(wù)的“可見性規(guī)則”,選擇合適的歷史版本進行讀取。這種機制讓“讀運行”和“寫操作”可以并行執(zhí)行,互不阻塞。
1.2 解決的核心痛點
在沒有MVCC的傳統(tǒng)鎖機制中,并發(fā)場景會面臨嚴重的性能問題:
- 讀阻塞寫:當事務(wù)A讀取某條資料時,會加共享鎖(S鎖),此時事務(wù)B要修改該內(nèi)容,需加排他鎖(X鎖),但S鎖和X鎖互斥,事務(wù)B會被阻塞;
- 寫阻塞讀:當事務(wù)A修改數(shù)據(jù)加X鎖時,事務(wù)B讀取該數(shù)據(jù)需加S鎖,同樣會被阻塞。
而MVCC依據(jù)“多版本”設(shè)計,讓讀操作讀取歷史版本,寫操作生成新版本,二者互不干擾。例如:
- 事務(wù)A修改數(shù)據(jù)時,生成新版本并加X鎖;
- 事務(wù)B讀取同一數(shù)據(jù)時,無需等待X鎖釋放,直接讀取未被修改的歷史版本;
- 雙方并行執(zhí)行,既保證了數(shù)據(jù)一致性,又提升了并發(fā)效率。
二、MVCC的核心原理:如何實現(xiàn)“多版本”與“可見性”?
MVCC的實現(xiàn)依賴InnoDB的三大核心組件,以及一套嚴格的“可見性判斷規(guī)則”,這也是理解MVCC的關(guān)鍵。
2.1 支撐MVCC的3個核心組件
InnoDB通過以下三個組件,為MVCC給出“版本存儲”和“版本追溯”的基礎(chǔ),這些組件在之前的InnoDB架構(gòu)文章中已有提及,此處需結(jié)合MVCC重新梳理:
1. 數(shù)據(jù)行的隱藏列
InnoDB會為每一條數(shù)據(jù)記錄自動添加3個隱藏列,用于記錄版本信息:
- DB_TRX_ID(事務(wù)ID):記錄最后一次修改該素材的事務(wù)ID(每個事務(wù)啟動時,InnoDB會分配一個全局唯一的遞增事務(wù)ID);
- DB_ROLL_PTR(回滾指針):指向該信息的“上一個歷史版本”在Undo Log中的存儲地址,通過該指針,可串聯(lián)起該數(shù)據(jù)的所有歷史版本,形成一條“版本鏈”;
- DB_ROW_ID(行ID):若表沒有顯式定義主鍵,InnoDB會用這個隱藏列作為默認主鍵,與MVCC直接關(guān)聯(lián)不大,但確保每行資料唯一。
舉個例子:假設(shè)表user有一條初始數(shù)據(jù)(id=1, name="張三", age=20),其隱藏列初始狀態(tài)如下:
| id | name | age | DB_TRX_ID | DB_ROLL_PTR |
|---|---|---|---|---|
| 1 | 張三 | 20 | 0 | NULL |
2. Undo Log(回滾日志)
Undo Log不僅是事務(wù)回滾的依據(jù),也是MVCC存儲“歷史版本數(shù)據(jù)”的載體。當事務(wù)修改材料時,InnoDB會先將數(shù)據(jù)的“舊版本”寫入Undo Log,再修改當前數(shù)據(jù)并更新隱藏列:
- 若事務(wù)執(zhí)行
ROLLBACK,可通過Undo Log恢復(fù)舊版本; - 若其他事務(wù)需要讀取歷史版本,可通過
DB_ROLL_PTR從Undo Log中獲取對應(yīng)版本數(shù)據(jù)。
例如:事務(wù)1(TRX_ID=100)執(zhí)行UPDATE user SET age=21 WHERE id=1,InnoDB會:
- 將數(shù)據(jù)的舊版本
(id=1, name="張三", age=20, DB_TRX_ID=0)寫入Undo Log; - 修改當前數(shù)據(jù)的
age為21,更新DB_TRX_ID=100,DB_ROLL_PTR指向Undo Log中舊版本的地址;
此時素材的版本鏈如下:
- 當前版本:
(age=21, DB_TRX_ID=100, DB_ROLL_PTR→Undo Log舊版本) - Undo Log中的歷史版本:
(age=20, DB_TRX_ID=0, DB_ROLL_PTR=NULL)
3. Read View(讀視圖)
Read View是事務(wù)讀取數(shù)據(jù)時的“可見性判斷依據(jù)”,它本質(zhì)是一個“事務(wù)ID集合”,包含以下4個核心參數(shù):
- m_low_limit_id:當前系統(tǒng)中“尚未分配的最小事務(wù)ID”(即下一個要啟動的事務(wù)ID);
- m_up_limit_id:當前Read View中“已分配的最大事務(wù)ID”;
- m_creator_trx_id:創(chuàng)建該Read View的事務(wù)ID(即當前讀取數(shù)據(jù)的事務(wù)ID);
- m_ids:當前環(huán)境中“正在活躍的事務(wù)ID列表”(即已啟動但未提交的事務(wù)ID)。
當事務(wù)讀取數(shù)據(jù)時,會通過Read View判斷數(shù)據(jù)版本的“可見性”—— 只有滿足規(guī)則的材料版本,才會被當前事務(wù)讀取。
2.2 MVCC的可見性判斷規(guī)則
事務(wù)讀取數(shù)據(jù)時,會從數(shù)據(jù)的“最新版本”開始,沿著DB_ROLL_PTR遍歷版本鏈,逐個判斷每個版本是否符合Read View的可見性規(guī)則,直到找到第一個“可見版本”。核心規(guī)則如下:
- 若當前版本的DB_TRX_ID = m_creator_trx_id:說明該版本是當前事務(wù)自己修改的,可見;
- 若當前版本的DB_TRX_ID < m_up_limit_id:
- 若DB_TRX_ID不在m_ids中(即修改該版本的事務(wù)已提交),可見;
- 若DB_TRX_ID在m_ids中(即修改該版本的事務(wù)未提交),不可見,繼續(xù)遍歷歷史版本;
- 若當前版本的DB_TRX_ID >= m_low_limit_id:說明該版本是在當前Read View創(chuàng)建后生成的,不可見,繼續(xù)遍歷歷史版本;
- 若m_up_limit_id ≤ DB_TRX_ID < m_low_limit_id:
- 若DB_TRX_ID不在m_ids中,可見;
- 若DB_TRX_ID在m_ids中,不可見,繼續(xù)遍歷歷史版本。
簡單來說:只有“已提交事務(wù)修改的版本”或“當前事務(wù)自己修改的版本”,才對當前事務(wù)可見。
三、MVCC的工作流程:結(jié)合實例看懂執(zhí)行過程
為了更直觀理解MVCC,此處憑借一個“雙事務(wù)并發(fā)”的實例,拆解其完整工作流程。假設(shè)場景如下:
- 初始數(shù)據(jù):
user(id=1, name="張三", age=20, DB_TRX_ID=0, DB_ROLL_PTR=NULL); - 事務(wù)A(TRX_ID=100):執(zhí)行
UPDATE user SET age=21 WHERE id=1,未提交; - 事務(wù)B(TRX_ID=200):執(zhí)行
SELECT * FROM user WHERE id=1,讀取數(shù)據(jù)。
步驟1:事務(wù)A修改材料,生成新版本
- 事務(wù)A啟動,InnoDB分配TRX_ID=100;
- 事務(wù)A執(zhí)行UPDATE操控:
- 將原數(shù)據(jù)
(age=20, DB_TRX_ID=0)寫入Undo Log; - 修改當前數(shù)據(jù)為
(age=21, DB_TRX_ID=100, DB_ROLL_PTR→Undo Log舊版本);
- 將原數(shù)據(jù)
- 事務(wù)A未提交,暫時持有數(shù)據(jù)的X鎖。
步驟2:事務(wù)B讀取數(shù)據(jù),創(chuàng)建Read View
- 事務(wù)B啟動,InnoDB分配TRX_ID=200;
- 事務(wù)B執(zhí)行SELECT操作,InnoDB為其創(chuàng)建Read View,此時架構(gòu)中活躍事務(wù)只有A(TRX_ID=100),因此Read View參數(shù)為:
- m_low_limit_id=201(下一個要分配的事務(wù)ID);
- m_up_limit_id=200(當前已分配的最大事務(wù)ID);
- m_creator_trx_id=200(事務(wù)B的ID);
- m_ids=[100](活躍事務(wù)ID列表)。
步驟3:事務(wù)B判斷版本可見性,讀取歷史版本
- 事務(wù)B首先讀取數(shù)據(jù)的“最新版本”(age=21,DB_TRX_ID=100);
- 按照可見性規(guī)則判斷:
- DB_TRX_ID=100 < m_up_limit_id=200,但100在m_ids(活躍事務(wù)列表)中,說明修改該版本的事務(wù)A未提交,此版本不可見;
- 沿著DB_ROLL_PTR遍歷歷史版本,讀取Undo Log中的舊版本(age=20,DB_TRX_ID=0);
- 再次判斷:
- DB_TRX_ID=0 < m_up_limit_id=200,且0不在m_ids中(事務(wù)已提交,實際是初始狀態(tài)),此版本可見;
- 事務(wù)B返回該可見版本的數(shù)據(jù):
(id=1, name="張三", age=20)。
步驟4:事務(wù)A提交后,事務(wù)B再次讀取
- 事務(wù)A提交,釋放X鎖,此時數(shù)據(jù)的最新版本(age=21,DB_TRX_ID=100)變?yōu)椤耙烟峤粻顟B(tài)”;
- 事務(wù)B再次執(zhí)行SELECT操作(若事務(wù)B未結(jié)束,InnoDB不會重新創(chuàng)建Read View,仍使用之前的Read View):
- 再次讀取最新版本(age=21,DB_TRX_ID=100);
- 按原Read View判斷:100仍在m_ids中(Read View未更新),此版本仍不可見;
- 繼續(xù)讀取歷史版本(age=20),返回結(jié)果不變。
這也解釋了InnoDB在“可重復(fù)讀(Repeatable Read)”隔離級別下,“事務(wù)內(nèi)多次讀取同一數(shù)據(jù),結(jié)果一致”的原因——Read View在事務(wù)首次讀取時創(chuàng)建,后續(xù)不會更新。
四、MVCC與事務(wù)隔離級別的關(guān)聯(lián)
MVCC的行為會隨InnoDB的事務(wù)隔離級別變化,核心差異在于“Read View的創(chuàng)建時機”不同,這直接影響讀取數(shù)據(jù)的可見性。InnoDB支持的4個隔離級別中,與MVCC相關(guān)的是“讀已提交(Read Committed)”和“可重復(fù)讀(Repeatable Read)”(另外兩個級別“讀未提交”不判斷版本可見性,“串行化”用鎖代替MVCC)。
4.1 讀已提交(Read Committed):每次讀取都創(chuàng)建新Read View
- Read View創(chuàng)建時機:事務(wù)中每次執(zhí)行SELECT操作時,都會
重新創(chuàng)建一個新的Read View; - 核心特點:能看到“當前時間點已提交的所有事務(wù)修改的版本”,避免“不可重復(fù)讀”;
- 實例驗證:
- 事務(wù)A(TRX_ID=100)修改數(shù)據(jù)為
age=21,未提交; - 事務(wù)B(TRX_ID=200)
首次SELECT,創(chuàng)建Read View(m_ids=[100]),讀取到age=20; - 事務(wù)A提交,數(shù)據(jù)最新版本變?yōu)?code>age=21(已提交);
- 事務(wù)B再次SELECT,
重新創(chuàng)建Read View(此時m_ids為空),讀取最新版本age=21;
結(jié)果:事務(wù)B兩次讀取結(jié)果不同,符合“讀已提交”的特性。
- 事務(wù)A(TRX_ID=100)修改數(shù)據(jù)為
4.2 可重復(fù)讀(Repeatable Read):事務(wù)首次讀取時創(chuàng)建Read View
- Read View創(chuàng)建時機:事務(wù)中首次執(zhí)行SELECT操作時創(chuàng)建Read View,后續(xù)所有SELECT都
復(fù)用該Read View; - 核心特點:事務(wù)內(nèi)多次讀取同一數(shù)據(jù),結(jié)果一致,避免“不可重復(fù)讀”和“幻讀”(InnoDB通過間隙鎖輔助解除幻讀);
- 實例驗證:
- 事務(wù)A(TRX_ID=100)修改數(shù)據(jù)為
age=21,未提交; - 事務(wù)B(TRX_ID=200)首次SELECT,創(chuàng)建Read View(
m_ids=[100]),讀取到age=20; - 事務(wù)A提交,數(shù)據(jù)最新版本變?yōu)閍ge=21(已提交);
- 事務(wù)B再次SELECT,復(fù)用原Read View(
m_ids仍為[100]),仍讀取到age=20;
結(jié)果:事務(wù)B兩次讀取結(jié)果一致,符合“可重復(fù)讀”的特性(MySQL默認隔離級別)。
- 事務(wù)A(TRX_ID=100)修改數(shù)據(jù)為
五、MVCC的實戰(zhàn)價值:工作中需要注意的點
MVCC縱然提升了并發(fā)性能,但在實際工作中,若使用不當,可能會引發(fā)性能挑戰(zhàn)或數(shù)據(jù)一致性風(fēng)險,需注意以下幾點:
5.1 長事務(wù)會導(dǎo)致Undo Log膨脹
由于MVCC依賴Undo Log存儲歷史版本,若存在“長事務(wù)”(如事務(wù)啟動后長時間不提交),InnoDB無法回收該事務(wù)可見的歷史版本對應(yīng)的Undo Log,會導(dǎo)致Undo Log文件持續(xù)增大,占用磁盤空間,同時也會增加版本鏈遍歷的時間,影響讀性能。
解決方案:
- 嚴格控制事務(wù)時長,避免在事務(wù)中包含用戶交互(如等待用戶輸入);
- 定期監(jiān)控長事務(wù),通過
information_schema.innodb_trx表查看未提交的事務(wù):SELECT trx_id, trx_started, trx_state, trx_query FROM information_schema.innodb_trx WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60; -- 查找運行超過60秒的事務(wù)
5.2 合理選擇隔離級別,平衡一致性與性能
讀已提交(Read Committed):適合對“
數(shù)據(jù)一致性要求不高,但并發(fā)性能要求高”的場景(如電商商品列表查詢),Read View每次創(chuàng)建,能及時看到已提交的新數(shù)據(jù),且鎖爭用少;可重復(fù)讀(Repeatable Read):適合對“
數(shù)據(jù)一致性要求高”的場景(如金融交易、訂單支付),但長事務(wù)下可能因Undo Log膨脹影響性能。
常見誤解:“數(shù)據(jù)一致性要求高 = 實時看到最新數(shù)據(jù)”??
很多人覺得 “場景寫反”,本質(zhì)是陷入了一個誤解:把 “數(shù)據(jù)一致性” 等同于 “實時看到最新數(shù)據(jù)”。但實際上,業(yè)務(wù)場景中的 “一致性需求”,核心是 “業(yè)務(wù)邏輯執(zhí)行過程中,數(shù)據(jù)不會被意外修改導(dǎo)致邏輯混亂”,而非 “必須看到每一刻的最新數(shù)據(jù)”。?
舉個更直觀的例子:?
場景 1(商品列表):用戶看的是 “快照數(shù)據(jù)”,即使有延遲或變化,不影響核心體驗;?
“流程化邏輯”,數(shù)據(jù)必須在整個流程中保持穩(wěn)定,否則邏輯會崩潰。?就是場景 2(訂單支付):框架執(zhí)行的
注意:若使用“可重復(fù)讀”級別,需避免長事務(wù);若業(yè)務(wù)允許,可將非核心查詢的隔離級別降為“讀已提交”,提升性能。
5.3 理解MVCC與鎖的關(guān)系:并非替代鎖
MVCC主導(dǎo)解決“讀-寫并發(fā)”的阻塞問題,但“寫-寫并發(fā)”仍需依賴鎖機制:
- 當兩個事務(wù)同時修改同一條數(shù)據(jù)時,仍會通過排他鎖(X鎖)互斥,避免數(shù)據(jù)沖突;
- MVCC與鎖機制是“互補關(guān)系”:MVCC處理讀-寫并發(fā),鎖處理寫-寫并發(fā),共同保障InnoDB的高并發(fā)能力。
六、總結(jié)
| 對比維度 | 讀已提交(Read Committed) | 可重復(fù)讀(Repeatable Read) |
|---|---|---|
| Read View 創(chuàng)建時機 | 每次執(zhí)行 SELECT 時重新創(chuàng)建 | 事務(wù)首次 SELECT 時創(chuàng)建,后續(xù)復(fù)用 |
| 一致性保障范圍 | 僅避免 “臟讀”,不避免 “不可重復(fù)讀” | 避免 “臟讀”+“不可重復(fù)讀”,配合間隙鎖避免 “幻讀” |
| 信息版本可見性 | 能看到 “當前時間點已提交的所有數(shù)據(jù)版本”(實時性強) | 僅能看到 “事務(wù)首次讀時已提交的版本”(版本固定) |
| 性能損耗點 | 鎖爭用少(無間隙鎖)、Undo Log 回收快(版本鏈短) | 鎖爭用多(有間隙鎖)、Undo Log 回收慢(版本鏈長) |
MVCC是InnoDB實現(xiàn)高并發(fā)讀寫的核心技術(shù),其本質(zhì)是通過“多版本數(shù)據(jù)”和“可見性判斷”,讓讀操作與寫操作并行執(zhí)行。理解MVCC,需要掌握三個核心:
- 組件:數(shù)據(jù)行隱藏列(版本標識)、Undo Log(版本存儲)、Read View(可見性判斷);
- 規(guī)則:基于Read View的版本可見性判斷邏輯;
- 隔離級別關(guān)聯(lián):Read View的創(chuàng)建時機決定了隔離級別的行為。
在實際工作中,合理利用MVCC的特性(如選擇合適的隔離級別),規(guī)避長事務(wù)導(dǎo)致的Undo Log膨脹問題,能讓InnoDB更好地支撐高并發(fā)業(yè)務(wù)。無論是開發(fā)工程師寫SQL,還是DBA做性能調(diào)優(yōu),理解MVCC都是必備的基礎(chǔ)能力。
Studying will never be ending.
▲如有紕漏,煩請指正~~

浙公網(wǎng)安備 33010602011771號