面試官:MySQL 為什么使用 MVCC?原理是什么?
大家好,我是君哥。
MVCC 中文名稱叫多版本并發控制,是 InnoDB 引擎為了提高并發效率引入的協議。今天來聊一聊 MVCC。
1.基礎知識
數據庫事務并發通常會遇到三個問題:
- 臟讀:事務 A 讀取了事務 B 未提交的修改數據。如果事務 B 回滾,事務 A 讀取的數據就是無效的臟數據。
- 不可重復讀:同一事務內多次讀取同一行數據,這條數據因為被其他事務修改過并且已經提交事務,導致多次讀取到的結果不一致。
- 幻讀:同一事務內多次查詢同一范圍內的數據,因其他事務插入或刪除符合條件的數據,導致事務在后面讀取到的結果集不一樣,像產生了幻覺。
其實出現幻讀也會造成不可重復,所以幻讀和不可重復讀有時容易混淆。不可重復度主要針對的是老數據的修改,而幻讀針對的是數據插入或數據刪除。
針對這三個并發問題,數據庫引入了隔離級別,不同隔離級別可以解決不同的問題。下面介紹的隔離級別隔離性依次變弱,并發性能依次變強。
串行化(Serializable):事務對數據讀寫都是串行化的。
可重復讀(Repeatable Read):事務執行過程中,多次讀取同一行數據,讀取結果一致。MySQL 默認隔離級別就是可重復讀。
讀已提交數據(Read Committed):事務執行過程中,如果有其他事務修改了數據并且提交事務,當前事務可以讀取到最新提交的數據。
讀未提交數據(Read Uncommitted):事務執行過程中,可以讀取到其他事務未提交的數據。
下表展示了這四種隔離級別對臟讀、幻讀、可重復讀的解決情況。
隔離級別/并發問題 | 臟讀 | 不可重復讀 | 幻讀 |
串行化 | x | x | x |
可重復度 | x | x | x |
讀已提交 | x | ? | ? |
讀未提交 | ? | ? | ? |
可重復讀并沒有完全解決幻讀,配合 MySQL 中的 Next-Key Lock 來解決。
2.MVCC
上面講了數據庫事務并發存在的問題和 MySQL 的事務隔離級別。那什么是 MVCC 呢?
2.1 版本鏈
MVCC 是對同一行數據,記錄多個事務的修改版本,這些版本串聯起來,保存在 undolog 中。
InnoDB 引擎在每行記錄中會添加了 3 個隱藏的列:
- DB_TRX_ID:修改(插入、更新或刪除)這一條數據的事務 id;
- DB_ROLL_PTR:回滾指針,指向修改前的歷史版本,用于回滾操作;
- DB_ROW_ID:當表中不定義主鍵時用作主鍵來自動生成聚簇索引。
MVCC 通過上面兩個字段,把每個事務修改后的數據和修改前的歷史版本串聯起來,形成一個版本鏈。
舉一個例子,我們有一張記錄賬戶余額的表 t_account,字段包括 id、account(賬戶)、amount(金額)。初始階段,id = 10,account = 1100 的這條記錄在事務 1 提交后這個賬戶剩余金額是 100,事務 2 把剩余金額改成了 150,事務 3 把剩余金額改成了 200。
如下圖,事務回滾的時候,可以根據 DB_ROLL_PTR 指向的版本,回滾到這個版本的數據。
圖片
2.2 ReadView
上面講了 MVCC 中的版本鏈,那如果現在有一個事務要讀取 id = 10,account = 1100 的這條記錄,這時候版本鏈上面有多個版本,這個事務應該讀取哪個版本呢?
這時我們引入一個新的概念 ReadView(讀視圖),用來控制當前事務應該讀取上面版本鏈中的那一個版本數據,它只作用于可重復讀和讀已提交這兩個隔離級別。它主要包含 4 個屬性:
MVCC 是指對同一行數據,記錄多個事務的修改版本,這些版本串聯起來,保存在 undolog 中。
InnoDB 引擎在每行記錄中會添加了 3 個隱藏的列:
- DB_TRX_ID:修改(插入、更新或刪除)這一條數據的事務 id;
- DB_ROLL_PTR:回滾指針,指向修改前的歷史版本,用于回滾操作;
- DB_ROW_ID:如果表中沒有定義主鍵,這個字段用作主鍵來自動生成聚簇索引。
ReadView 對可重復讀和讀已提交這 2 個隔離級別來說,有下面的不同:
- 已提交讀:事務中每次查詢操作,都會創建一個新的 ReadView。在上面的例子中,m_ids 集合是 {2,3},這時事務 4 開始,查詢 t_account 中 id = 10 的記錄,會新建一個 ReadView,查詢到 amount = 100,如果事務 4 執行過程中,事務 2 提交,事務 4 中再次查詢查詢 t_account 中 id = 10 的記錄,會再次創建一個 ReadView,查到 amount = 150。如下圖:
圖片
- 可重復讀:只有事務開始的時候,創建一個新的 ReadView,后面的讀操作都公用這個 ReadView。在上面的例子中,m_ids 集合是 {2,3},這時事務 4 開始,查詢 t_account 中 id = 10 的記錄,會創建一個 ReadView,查詢到 amount = 100,如果事務 4 執行過程中,事務 2 提交,事務 4 中再次查詢查詢 t_account 中 id = 10 的記錄,還是使用之前的 ReadView,查到 amount = 100。如下圖:
圖片
2.3 修改隔離級別
其實在實際使用中,我們在一個事務中很少用到重復讀的情況,這種情況多數是代碼寫的有問題。所以好多公司會修改 MySQL 的默認隔離級別,改成讀已提交。
改成讀已提交還有一個好處就是可以減少死鎖發生。
當然,讀已提交不能解決幻讀問題。比如在一個事務中,查詢了兩次訂單量,兩次查詢中間又有新訂單生成,訂單數量會發現不一樣。這類情況就要看業務上能不能接受了。
總結
MVCC 是 MySQL 中非常重要的一個并發優化,從事務隔離級別、版本鏈、ReadView 這幾個方面著手,很容易理解 MVCC 的原理。


































