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


































