MySQL InnoDB 事務(wù)隔離與 MVCC、版本鏈與 ReadView 原理詳解
我去面試的時(shí)候滔滔不絕,感覺勝利在握,可是面試官忽然到:“什么是 MVCC,MySQL 有了各種鎖?為什么還要射界 MVCC?”
這是個(gè)好問題!
MVCC(Multi-Version Concurrency Control)中文叫做多版本并發(fā)控制協(xié)議,是 MySQL InnoDB 引擎用于控制數(shù)據(jù)并發(fā)訪問的協(xié)議。
今天我就帶你從 MVCC 基本原理說起,并且教你鬼狐一般隔離級別、版本連、Read View 的作用。
為什么需要 MVCC
在 MVCC 出現(xiàn)之前,數(shù)據(jù)庫主要依靠鎖機(jī)制來解決并發(fā)沖突。但鎖機(jī)制存在明顯的瓶頸:
鎖的代價(jià):
- 阻塞等待:寫鎖會(huì)阻塞讀鎖,讀鎖會(huì)阻塞寫鎖
- 死鎖風(fēng)險(xiǎn):多個(gè)事務(wù)相互等待對方釋放鎖
- 并發(fā)度低:悲觀鎖機(jī)制限制了系統(tǒng)吞吐量
-- 傳統(tǒng)鎖機(jī)制下的并發(fā)問題示例
-- 事務(wù)1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; -- 獲取寫鎖
-- 事務(wù)2(被阻塞)
BEGIN;
SELECT balance FROM accounts WHERE user_id = 1; -- 等待讀鎖,直到事務(wù)1提交試想一下,如果一個(gè)線程準(zhǔn)備執(zhí)行 UPDATE 一行數(shù)據(jù),如果這時(shí)候阻塞住了所有的 SELECT 語句,那么這個(gè)性能你能接受嗎?
余弦:“那肯定不行,所以 MVCC 的核心思想是為每個(gè)數(shù)據(jù)項(xiàng)維護(hù)多個(gè)版本,讀寫操作可以并發(fā)進(jìn)行而不相互阻塞?”
聰明,MVCC 的核心思想是為每一行數(shù)據(jù)維護(hù)多個(gè)版本(通常是兩個(gè)),通過某個(gè)時(shí)間點(diǎn)的“快照”(Snapshot)來讀取數(shù)據(jù),從而避免加鎖帶來的性能損耗,實(shí)現(xiàn)非阻塞的讀操作:
- 非阻塞讀:讀操作不需要等待寫操作完成
- 非阻塞寫:寫操作不需要等待讀操作完成(在合理隔離級別下)。
- 高并發(fā):大幅提升系統(tǒng)吞吐量。
隔離級別
在深入理解 MVCC 之前,我們現(xiàn)在還需要進(jìn)步一了解和 MVCC 緊密關(guān)聯(lián)的概念,隔離級別。
MySQL 的事務(wù)隔離級別有以下四種:
- 讀未提交(Read Uncommitted):
- 是指一個(gè)事務(wù)可以看到另外一個(gè)事務(wù)尚未提交的修改。
- 設(shè)置:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - 實(shí)現(xiàn)機(jī)制:不加任何鎖,直接讀取最新的數(shù)據(jù)頁,無論其是否已提交。性能最好,但數(shù)據(jù)一致性最差,生產(chǎn)環(huán)境極少使用。
- 讀已提交(Read Committed,簡寫 RC):
- 如何避免臟讀:因?yàn)槊看巫x都取已提交的最新快照,所以不會(huì)讀到未提交的數(shù)據(jù)。
- 為何有不可重復(fù)讀和幻讀:因?yàn)槊看尾樵兊?ReadView 都最新,其他事務(wù)的提交會(huì)立刻被當(dāng)前事務(wù)看到。
- 是指一個(gè)事務(wù)只能看到已經(jīng)提交的事務(wù)的修改,如果在事務(wù)執(zhí)行過程中有別的事務(wù)提交了,那么事務(wù)還是能夠看到別的事務(wù)最新提交的修改。
- 設(shè)置:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;(RC) - 實(shí)現(xiàn)機(jī)制:基于 MVCC,每次 SELECT 語句都會(huì)生成一個(gè)獨(dú)立的、最新的 ReadView。
- 可重復(fù)讀(Repeatable Read,簡寫 RR):
- 如何避免不可重復(fù)讀:因?yàn)檎麄€(gè)事務(wù)看到的都是同一個(gè)“歷史快照”,其他事務(wù)的提交對當(dāng)前事務(wù)不可見。
- InnoDB 如何解決幻讀:這是 MySQL InnoDB 的精髓!通過 Next-Key Lock(臨鍵鎖) 實(shí)現(xiàn)。它結(jié)合了記錄鎖(鎖住索引項(xiàng))和間隙鎖(鎖住索引項(xiàng)之間的間隙)。當(dāng)執(zhí)行范圍查詢時(shí),InnoDB 會(huì)鎖住整個(gè)范圍,防止其他事務(wù)在這個(gè)范圍內(nèi)插入新數(shù)據(jù),從而避免了幻讀。
- 是指在這一個(gè)事務(wù)內(nèi)部讀同一個(gè)數(shù)據(jù)多次,讀到的結(jié)果都是同一個(gè)。這意味著即便在事務(wù)執(zhí)行過程中有別的事務(wù)提交,這個(gè)事務(wù)依舊看不到別的事務(wù)提交的修改。這是 MySQL 默認(rèn)的隔離級別。
- 設(shè)置:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;(RR) - 實(shí)現(xiàn)機(jī)制:基于 MVCC,但一個(gè)事務(wù)中只有第一次 SELECT 會(huì)生成 ReadView,后續(xù)所有讀操作都復(fù)用這個(gè) ReadView。
-- 事務(wù)A
BEGIN;
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE; -- 會(huì)鎖住age=20到30這個(gè)區(qū)間,甚至包括兩邊的“間隙”
-- 此時(shí)事務(wù)B的插入會(huì)被阻塞
INSERT INTO users (name, age) VALUES ('新用戶', 25); -- 阻塞,直到事務(wù)A提交- 串行化(Serializable)是指事務(wù)對數(shù)據(jù)的讀寫都是串行化的。
- 設(shè)置:
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; - 實(shí)現(xiàn)機(jī)制:完全摒棄 MVCC,退化為基于鎖的并發(fā)控制。所有讀操作都會(huì)加上共享鎖,讀寫操作會(huì)相互阻塞。一致性最好,但并發(fā)性能最差,像單線程執(zhí)行一樣。
余弦:為什么要隔離?
在并發(fā)環(huán)境下,如果不進(jìn)行任何隔離控制,并發(fā)事務(wù)會(huì)引發(fā)哪些問題?隔離級別本質(zhì)上就是為了解決這些問題而存在的。
臟讀 (Dirty Read)
一個(gè)事務(wù)讀到了另一個(gè)未提交事務(wù)修改的數(shù)據(jù)。如果另一個(gè)事務(wù)中途回滾,那么第一個(gè)事務(wù)讀到的數(shù)據(jù)就是“臟”的、無效的。
示例:
- 事務(wù) A 將賬戶余額從 100 元修改為 200 元(但未提交)。
- 此時(shí)事務(wù) B 讀取余額,得到了 200 元這個(gè)結(jié)果。
- 事務(wù) A 因某種原因回滾,余額恢復(fù)為 100 元。
- 事務(wù) B 之后的操作都是基于錯(cuò)誤的“200 元”余額進(jìn)行的。
圖片
不可重復(fù)讀 (Non-Repeatable Read)
一個(gè)事務(wù)內(nèi),兩次讀取同一個(gè)數(shù)據(jù)項(xiàng),得到了不同的結(jié)果。重點(diǎn)在于另一個(gè)已提交事務(wù)對數(shù)據(jù)進(jìn)行了修改(UPDATE)。
示例:
- 事務(wù) A 第一次讀取賬戶余額為 100 元。
- 此時(shí)事務(wù) B 提交了修改,將余額更新為 150 元。
- 事務(wù) A 再次讀取余額,得到了 150 元。兩次讀取結(jié)果不一致。
圖片
幻讀 (Phantom Read)
一個(gè)事務(wù)內(nèi),兩次執(zhí)行同一個(gè)查詢,返回的結(jié)果集行數(shù)不同。重點(diǎn)在于另一個(gè)已提交事務(wù)對數(shù)據(jù)進(jìn)行了增刪(INSERT/DELETE),像產(chǎn)生了幻覺一樣。
示例:
- 事務(wù) A 第一次查詢年齡小于 30 歲的員工,返回了 10 條記錄。
- 此時(shí)事務(wù) B 提交了一個(gè)新操作,插入了一名 25 歲的員工記錄。
- 事務(wù) A 再次執(zhí)行相同的查詢,返回了 11 條記錄。

不可重復(fù)讀 vs 幻讀:
- 不可重復(fù)讀針對的是某一行數(shù)據(jù)的值被修改(UPDATE)。
- 幻讀針對的是結(jié)果集的行數(shù)發(fā)生變化(INSERT/DELETE)。
SQL 標(biāo)準(zhǔn)下的四種事務(wù)隔離級別
為了解決上述問題,SQL 標(biāo)準(zhǔn)定義了四種隔離級別,嚴(yán)格程度從低到高。級別越高,能解決的問題越多,但并發(fā)性能通常越低。
隔離級別 | 臟讀 | 不可重復(fù)讀 | 幻讀 |
讀未提交 (Read Uncommitted) | ? 可能 | ? 可能 | ? 可能 |
讀已提交 (Read Committed) | ? 避免 | ? 可能 | ? 可能 |
可重復(fù)讀 (Repeatable Read) | ? 避免 | ? 避免 | ? 可能 |
串行化 (Serializable) | ? 避免 | ? 避免 | ? 避免 |
注意: 在 MySQL 的 InnoDB 引擎中,通過 Next-Key Locking 技術(shù),在可重復(fù)讀(Repeatable Read) 隔離級別下就已經(jīng)可以避免絕大部分的幻讀現(xiàn)象。
這是 MySQL 對標(biāo)準(zhǔn)隔離級別的增強(qiáng),也是其默認(rèn)使用該級別的重要原因。
另外,還有兩個(gè)概念你需要掌握:
- 快照讀:快照讀就是在事務(wù)開始的時(shí)候創(chuàng)建了一個(gè)數(shù)據(jù)的快照,在整個(gè)事務(wù)過程中都讀這個(gè)快照;
- 當(dāng)前讀,則是每次都去讀最新數(shù)據(jù)。MySQL 在可重復(fù)讀這個(gè)隔離級別下,查詢的執(zhí)行效果和快照讀非常接近。































