1.為什么需要MVCC
在并發場景下,讀寫操作會面臨嚴重的沖突問題:
1.讀操作如果遇到寫操作,要么“讀到未提交的臟數據”,要么“被寫操作阻塞(等待鎖釋放)”;
2.寫操作如果遇到讀操作,要么“覆蓋讀操作需要的數據”,要么“被讀操作阻塞”;
MVCC通過“多版本”解決了這個問題:寫操作會生成新的數據版本,讀操作讀取舊的版本(不影響寫),兩者不干擾。
2.什么是MVCC
MySQL的MVCC(Multi-Version Concurrency Control,多版本并發控制)是InnoDB存儲引擎實現高并發讀寫的核心操作。核心思想:為數據庫中的每條記錄維護多個版本,通過“版本鏈”和“可間性判斷規則”,讓不同實物在并發訪問時,能夠看到符合自己隔離級別的數據版本,從而避免讀寫沖突(讀不阻塞寫,寫不阻塞讀),同事保證事務隔離性。
3.MVCC的核心組成
MVCC的實現依賴三個關鍵組件:隱藏列(版本標識),Undo Log(版本存儲),Read View(可見性判斷)。
3.1 隱藏列:記錄的“版本身份證”
InnoDB為表中的每條記錄添加了三個隱藏字段,用于記錄標識的版本信息:
- DB_TRX_ID:記錄最后一次修改該記錄的事務ID(6字節)。每次事務對記錄執行insert/update/delete時,都會將自己的事務ID寫入該字段。
- DB_ROLL_PTR:回滾指針(7字節)。指向該記錄的“上個版本”在Undo Log中的位置(通過它可以串聯所有的歷史版本,形成“版本鏈”)。
- DB_ROW_ID:記錄的唯一標識(6字節)。如果表沒有定義主鍵或唯一索引,InnoDB會用它作為默認聚簇索引(一般用不到,可忽略)。
3.2 Undo Log:版本的“歷史記錄館”
Undo Log(回滾日志)是InnoDB用于存儲“記錄舊版本”的空間。當視為修改記錄時,舊版本的數據不會被直接刪除,而是被寫入Undo Log,供后序“回滾”或“其他事務讀取”使用。
與隱藏列相結合形成“版本鏈”,記錄的“版本鏈”形成的過程如下:
- 初始插入一條數據時,DB_TRX_ID是插入事務的ID,DB_ROLL_PTR為null(無歷史版本,新數據無歷史版本)。
- 當事務A(事務ID=110)修改該記錄時,InnoDB會先將舊版本數據寫入Undo Log,然后更新記錄的DB_TRX_ID=110,并將DB_ROLL_PTR指向Undo Log中的舊版本。
- 之后事務B(事務ID=220)再次修改該記錄時,會將“事務A修改后的版本”寫入Undo Log,更新DB_TRX_ID=220,DB_ROLL_PTR指向事務A版本再Undo Log的位置。
- 最終,通過DB_ROLL_PTR串聯的Undo Log記錄,形成了改記錄的“版本鏈”(最新版本在表中,歷史版本在Undo Log中)。
一條記錄的版本鏈結構(簡化):
當前記錄(表中):data:(name='HajiHang',age=21)|DB_TRX_ID=220|DB_ROLL_PTR——>Undo Log中的版本1
Undo Log:
Undo Log中的版本1(事務A修改后的版本):
data:(name='HajiHang',age=20)|DB_TRX_ID=110|DB_ROLL_PTR——>Undo Log中的版本0
Undo Log中的版本0(初始插入版本):
data:(name='HajiHang',age=19)|DB_TRX_ID=55|DB_ROLL_PTR——>null
3.3 Read View:版本的“可見性過濾器”
有了版本鏈后,事務如何判斷“哪個版本的數據對自己可見”?這就需要Read View(讀試圖),它是一個“可見性判斷規則集合”,用于確定當前事務能夠看到版本鏈中的那個版本。
Read View包含4個核心變量(生成時確定):
- m_ids:生成Read View時,當前所有“活躍事務”(已啟動但未提交)的事務ID集合(無序)
- min_trx_id:m_ids中的最小事務ID(當前活躍事務中最早啟動的那個)
- max_trx_id:生成Read View時,“下一個將要分配的事務ID”(并非m_ids中的最大值,而是一個預分配的自增ID)
- creator_trx_id:當前生成Read View的事務自己的ID
4.可見性判斷規則(核心)
設記錄的版本對應的事務ID為trx_id,根據trx_id和Read View的變量對比:
- 如果trx_id == creator_trx_id:該版本是當前事務自己修改的,可見(自己改的自己當然可以看到)
- 如果trx_id < min_trx_id:該版本對應的事務在“當前Read View生成前”就已提交(因為它的ID比所有活躍事務的最小ID還小),可見。
- 如果trx_id >= max_trx_id:該版本對應的事務在“當前Read View生成后”才啟動(ID超過預分配的最大ID),不可見(還沒提交,或剛啟動)。
- 如果min_trx_id <=trx_id <=max_trx_id:
- 若trx_id在m_ids(活躍事務集合)中:說明該事務在Read View生成時還沒提交,不可見;
- 若trx_id不在m_ids中:說明該事務在Read View生成前已提交,可見;
5.MVCC與隔離級別的關系
MVCC的核心作用是實現隔離級別(ACID中的“I”隔離性)。InnoDB通過“Read View的生成時機”和“版本鏈”,在不同隔離級別下表現出不同的行為:
隔離級別 | MVCC行為(Read View生成時機) | 解決的問題 |
Read Uncommitted(讀未提交) | 不使用MVCC(直接讀取最新的版本) | 可能讀到未提交的臟數據(臟讀) |
Read Committed(讀已提交) | 每次執行select時,重新生成Read View | 避免臟讀(只看到已提交的版本);但是可能出現“不可重復讀”(兩次查詢Read View不同) |
Repeatable Read(可重復讀) | 僅在事務第一次select生成Read View,之后復用 | 避免不可重復讀(兩次查詢用同一個Read View,版本一致);但可能出現“幻讀”(通過間隙鎖解決) |
Serializable(串行化) | 不依賴MVCC,直接通過加鎖(讀加共享鎖,寫加排它鎖)實現 | 完全串行化,無并發問題,性能低 |
6.補充:不同隔離級別涉及到的并發問題
不同隔離級別會產生不同的并發問題,核心問題包括臟讀,不可重復讀,幻讀。
- 臟讀指的是一個事務讀取到另一個為提交事務修改的數據。(事務A在修改name并未提交,事務B讀取到了name,后事務A又進行修改并提交/或直接回滾,導入事務B讀取到了一個臨時,無效的數據)
- 不可重復讀指的是同一事務內,多次讀取同一數據,結果因其他已提交事務的修改而不同。(事務A第一次讀取name='HajiHang',事務B執行了修改操作將name改成了Hajimi,事務A再次讀取name發現不是HajiHang了)
- 幻讀指的是同一事務內,多次執行相同的查詢操作(通常是范圍查詢)時,結果集行數因其他一提交事務的插入/刪除而變化(事務A查詢score=90的人發現有5條數據,之后事務B插入一個90的數據,事務A再次查詢score=90發現結果集變為6條)