Mysql數(shù)據(jù)庫幻讀問題舉例詳解
前言
我們來詳細(xì)地聊一聊 MySQL InnoDB 中的“幻讀”(Phantom Read)問題。這是一個(gè)在數(shù)據(jù)庫事務(wù)隔離中非常核心且有時(shí)令人困惑的概念。
我會(huì)從定義、例子、原因以及解決方案幾個(gè)方面來徹底講清楚。
1. 什么是幻讀?
官方定義:幻讀指的是在一個(gè)事務(wù)內(nèi),相同的查詢在不同時(shí)間執(zhí)行,返回了不同數(shù)量的行。
這聽起來和“不可重復(fù)讀”很像,但它們有關(guān)鍵區(qū)別:
不可重復(fù)讀 (Non-Repeatable Read):側(cè)重于同一行的數(shù)據(jù)內(nèi)容被修改或刪除。(例如:你第二次查詢時(shí),某行的薪水從10000變成了12000)。
幻讀 (Phantom Read):側(cè)重于新的行被插入(或刪除),導(dǎo)致結(jié)果集的行數(shù)發(fā)生了變化。(針對(duì)結(jié)果集的數(shù)量變化,例如:你第一次查詢有10條記錄,第二次查詢卻冒出了11條)。
簡(jiǎn)單比喻:
不可重復(fù)讀:你碗里的一塊紅燒肉被別人咬了一口(數(shù)據(jù)內(nèi)容變了)。
幻讀:你正準(zhǔn)備夾碗里最后一塊紅燒肉時(shí),別人突然又往碗里加了一塊新的肉(數(shù)據(jù)行數(shù)變了)。
2. 幻讀發(fā)生的場(chǎng)景與例子
幻讀發(fā)生的根本原因是:在“可重復(fù)讀(REPEATABLE READ)”及以下隔離級(jí)別中,普通的一致性讀(快照讀)無法阻止其他事務(wù)插入新的、滿足當(dāng)前查詢條件的數(shù)據(jù)。
我們來看一個(gè)經(jīng)典的例子。
表結(jié)構(gòu):
CREATE TABLE `employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`salary` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_salary` (`salary`)
) ENGINE=InnoDB;
INSERT INTO `employee` (`name`, `salary`) VALUES
('Alice', 8000),
('Bob', 9000),
('Charlie', 10000);時(shí)間線:
| 時(shí)間 | 事務(wù)A (隔離級(jí)別:RR) | 事務(wù)B |
|---|---|---|
| T1 | START TRANSACTION; | |
| T2 | SELECT * FROM employee WHERE salary > 8000;結(jié)果: Bob, 9000 Charlie, 10000 (2 rows) | START TRANSACTION; |
| T3 | INSERT INTO employee (name, salary) VALUES ('David', 9500);COMMIT; <!-- 事務(wù)B提交 --> | |
| T4 | SELECT * FROM employee WHERE salary > 8000;結(jié)果: Bob, 9000 Charlie, 10000 (仍然是2 rows!) (這里沒有幻讀,因?yàn)镽R級(jí)別通過MVCC提供了快照) | |
| T5 | UPDATE employee SET salary = 8888 WHERE salary > 8000;(注意:這個(gè)更新操作是當(dāng)前讀,會(huì)看到事務(wù)B已提交的修改) | |
| T6 | SELECT * FROM employee WHERE salary > 8000;結(jié)果: Bob, 8888 Charlie, 8888 David, 8888 (3 rows! 幻讀出現(xiàn)了!) |
例子分析:
T2時(shí)刻:事務(wù)A第一次查詢,得到2條記錄。
T4時(shí)刻:事務(wù)A第二次普通查詢(快照讀)。由于InnoDB的MVCC(多版本并發(fā)控制)機(jī)制,它會(huì)讀取事務(wù)開始時(shí)的數(shù)據(jù)快照,所以看不到事務(wù)B新插入的
David(9500)。此時(shí)還沒有幻讀。T5時(shí)刻:關(guān)鍵點(diǎn)來了!事務(wù)A執(zhí)行了一個(gè)UPDATE操作。UPDATE/DELETE/INSERT 這類寫操作會(huì)使用“當(dāng)前讀”(Current Read),它會(huì)讀取數(shù)據(jù)庫中最新的、已提交的數(shù)據(jù)。因此,它看到了事務(wù)B插入的
David(9500)這條記錄,并將其薪水也更新為8888。T6時(shí)刻:事務(wù)A再次查詢。因?yàn)橹暗腢PDATE操作屬于當(dāng)前事務(wù)的修改,所以MVCC規(guī)則允許它看到自己的修改。于是,它神奇地看到了三條記錄!幻讀就在這一刻發(fā)生了。
這個(gè)例子展示了InnoDB中幻讀最典型的特征:即使是在默認(rèn)的RR隔離級(jí)別下,先快照讀,再當(dāng)前讀進(jìn)行寫操作,可能會(huì)意外地影響新插入的行,從而導(dǎo)致數(shù)據(jù)不一致。
3. 解決方案:Next-Key Lock 鎖機(jī)制
InnoDB引擎為了解決幻讀問題,在“可重復(fù)讀(REPEATABLE READ)”隔離級(jí)別下就引入了一種叫做 Next-Key Lock 的鎖機(jī)制。它實(shí)際上是 記錄鎖(Record Lock) 和 間隙鎖(Gap Lock) 的結(jié)合。
記錄鎖 (Record Lock):鎖住索引上的某一條具體記錄。
間隙鎖 (Gap Lock):鎖住索引記錄之間的“間隙”,防止在這個(gè)間隙內(nèi)插入新的數(shù)據(jù)。它是一個(gè)左開右開的區(qū)間
(a, b)。臨鍵鎖 (Next-Key Lock):是記錄鎖 + 間隙鎖的結(jié)合。它鎖住一條記錄和它前面的間隙。它是一個(gè)左開右閉的區(qū)間
(a, b]。
如何解決幻讀?
在上面的例子中,如果事務(wù)A在第一次查詢時(shí),就對(duì) salary > 8000 這個(gè)條件加上了鎖,那么事務(wù)B的插入操作就會(huì)被阻塞,從而杜絕幻讀。
讓我們重演時(shí)間線,但這次事務(wù)A加鎖查詢:
| 時(shí)間 | 事務(wù)A (加鎖查詢) | 事務(wù)B |
|---|---|---|
| T1 | START TRANSACTION; | |
| T2 | SELECT * FROM employee WHERE salary > 8000 FOR UPDATE;(FOR UPDATE 會(huì)給查詢結(jié)果加Next-Key Lock) 結(jié)果:2 rows | START TRANSACTION; |
| T3 | INSERT INTO employee (name, salary) VALUES ('David', 9500);(這條語句會(huì)被阻塞,一直等待事務(wù)A釋放鎖!) | |
| T4 | SELECT ... FOR UPDATE; (再次查詢,結(jié)果一致) | ...(阻塞中)... |
| T5 | COMMIT; (提交事務(wù),釋放鎖) | ...(阻塞結(jié)束)... |
| T6 | (此時(shí)事務(wù)B才能成功插入) |
發(fā)生了什么?
當(dāng)事務(wù)A執(zhí)行 SELECT ... FOR UPDATE 時(shí),InnoDB會(huì)為其加Next-Key Lock。假設(shè) salary 上有二級(jí)索引 idx_salary,它可能會(huì)鎖住以下區(qū)間:
鎖住
(8000, 9000]這個(gè)Next-Key Lock(鎖住9000這條記錄和它前面的間隙)。鎖住
(9000, 10000]這個(gè)Next-Key Lock(鎖住10000這條記錄和它前面的間隙)。鎖住
(10000, +∞]這個(gè)Next-Key Lock(鎖住正無窮的上界)。
事務(wù)B試圖插入 salary = 9500 的記錄,這個(gè)值落在被事務(wù)A鎖住的 (9000, 10000] 間隙內(nèi),因此插入操作會(huì)被阻塞,直到事務(wù)A提交釋放鎖。這樣就徹底防止了幻讀的發(fā)生。
總結(jié)與最佳實(shí)踐
| 特性 | 說明 |
|---|---|
| 幻讀本質(zhì) | 同一事務(wù)內(nèi),兩次查詢結(jié)果集行數(shù)不一致, due to 其他事務(wù)的插入或刪除操作。 |
| InnoDB的默認(rèn)防御 | 在REPEATABLE READ隔離級(jí)別下,InnoDB通過 Next-Key Lock 機(jī)制來防止幻讀。 |
| 何時(shí)會(huì)發(fā)生幻讀 | 即使是在RR級(jí)別下,如果你只是進(jìn)行普通的快照讀(SELECT),然后基于此進(jìn)行當(dāng)前讀的寫操作(UPDATE/INSERT/DELETE),仍然可能遇到幻讀。快照讀不加鎖是根源。 |
| 徹底解決方法 | 在需要絕對(duì)保證數(shù)據(jù)一致性的關(guān)鍵操作中,使用 加鎖讀: 1. SELECT ... FOR UPDATE; (加寫鎖,阻塞其他事務(wù)的寫和加鎖讀)2. SELECT ... LOCK IN SHARE MODE; (加讀鎖,阻塞其他事務(wù)的寫)這些語句會(huì)在符合條件的索引上加Next-Key Lock,從而阻止其他事務(wù)在鎖定區(qū)間內(nèi)插入新數(shù)據(jù)。 |
| 終極方案 | 將事務(wù)隔離級(jí)別提升至 SERIALIZABLE。在這個(gè)級(jí)別下,所有的讀操作都會(huì)默認(rèn)加上類似 LOCK IN SHARE MODE 的鎖,幻讀自然不會(huì)發(fā)生,但這是以犧牲并發(fā)性能為代價(jià)的,一般不建議使用。 |
核心要點(diǎn):MySQL InnoDB 在 RR 級(jí)別下已經(jīng)通過 Next-Key Lock 很大程度上解決了幻讀問題。但你需要清楚地知道,只有在你的查詢語句確實(shí)需要加鎖(例如使用了 FOR UPDATE)或者涉及寫操作時(shí),Next-Key Lock 才會(huì)生效。單純的快照讀是無法完全避免幻讀的潛在影響的。

到此這篇關(guān)于Mysql數(shù)據(jù)庫幻讀問題的文章就介紹到這了,更多相關(guān)Mysql幻讀詳解內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mysql如何定時(shí)自動(dòng)新增分區(qū)
這篇文章主要介紹了mysql如何定時(shí)自動(dòng)新增分區(qū)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09
詳解Windows?Server?2012下安裝MYSQL5.7.24的問題
這篇文章主要介紹了Windows?Server?2012下安裝MYSQL5.7.24的詳細(xì)過程,本文通過圖文并茂實(shí)例代碼相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02
mybatis分頁插件pageHelper詳解及簡(jiǎn)單實(shí)例
這篇文章主要介紹了mybatis分頁插件pageHelper詳解及簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-05-05
如何設(shè)置才能遠(yuǎn)程登錄Mysql數(shù)據(jù)庫
本地機(jī)器安裝的數(shù)據(jù)庫,本地程序可以訪問,但是同事的機(jī)器卻無法連接訪問,發(fā)現(xiàn)是mysql數(shù)據(jù)庫沒有開啟遠(yuǎn)程訪問。那么我們需要如何設(shè)置呢,這就是本文探討的內(nèi)容了2014-08-08
簡(jiǎn)單分析MySQL中的primary key功能
這篇文章主要介紹了MySQL中的primary key功能,包括講到了其對(duì)InnoDB使用的影響,需要的朋友可以參考下2015-05-05
MySQL修改默認(rèn)存儲(chǔ)引擎的實(shí)現(xiàn)方法
下面小編就為大家?guī)硪黄狹ySQL修改默認(rèn)存儲(chǔ)引擎的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-03-03
3步搞定純真IP數(shù)據(jù)導(dǎo)入到MySQL的方法詳解
免編程,3步搞定純真IP數(shù)據(jù)導(dǎo)入到MySQL詳解,好多做ip地址查詢的朋友用的到。2009-10-10

