MySQ中出現(xiàn)幻讀問(wèn)題的解決過(guò)程
想象一下這樣的場(chǎng)景:
你在電商平臺(tái)購(gòu)物時(shí),看到某商品顯示"庫(kù)存僅剩3件"。當(dāng)你準(zhǔn)備下單時(shí),系統(tǒng)突然提示"庫(kù)存不足"。檢查后發(fā)現(xiàn),在你查看頁(yè)面和點(diǎn)擊購(gòu)買(mǎi)之間的短暫瞬間,其他用戶已經(jīng)買(mǎi)走了所有庫(kù)存。
這種"明明看到有貨卻買(mǎi)不到"的現(xiàn)象,在數(shù)據(jù)庫(kù)中就被稱為"幻讀"(Phantom Read)。
今天,我們將從底層原理到實(shí)際應(yīng)用,全面解析MySQL InnoDB引擎如何解決這一棘手問(wèn)題。
一、幻讀的準(zhǔn)確定義與核心特征
幻讀(Phantom Read)是指在一個(gè)事務(wù)內(nèi),連續(xù)執(zhí)行兩次相同的查詢,第二次查詢看到了第一次查詢沒(méi)有看到的"幻影行"(Phantom Rows)。這種現(xiàn)象特指其他事務(wù)插入了新記錄導(dǎo)致的問(wèn)題。
要深入理解幻讀,我們需要明確幾個(gè)關(guān)鍵特征:
- 行級(jí)變化:幻讀關(guān)注的是新行的出現(xiàn),而不是已有行的修改(那是不可重復(fù)讀的問(wèn)題)
- 范圍查詢:通常發(fā)生在范圍查詢(如WHERE id > 100)而非精確匹配查詢
- 寫(xiě)操作影響:幻讀會(huì)對(duì)UPDATE、DELETE等操作產(chǎn)生影響,可能導(dǎo)致數(shù)據(jù)不一致

這個(gè)流程圖展示了一個(gè)典型的幻讀導(dǎo)致業(yè)務(wù)問(wèn)題的場(chǎng)景:事務(wù)A基于初始查詢結(jié)果執(zhí)行UPDATE操作時(shí),意外影響了事務(wù)B插入的新記錄,導(dǎo)致數(shù)據(jù)不一致。
幻讀 vs 不可重復(fù)讀
很多開(kāi)發(fā)者容易混淆幻讀和不可重復(fù)讀,讓我們通過(guò)表格明確它們的區(qū)別:
| 特征 | 不可重復(fù)讀 | 幻讀 |
|---|---|---|
| 關(guān)注點(diǎn) | 同一行數(shù)據(jù)的值變化 | 新行的出現(xiàn)或消失 |
| 操作類型 | UPDATE操作導(dǎo)致 | INSERT/DELETE操作導(dǎo)致 |
| 查詢方式 | 精確匹配查詢 | 范圍查詢 |
| 解決方案 | 行鎖或MVCC | 間隙鎖或串行化 |
二、MySQL隔離級(jí)別深度解析
理解了幻讀現(xiàn)象后,我們需要全面了解MySQL的隔離級(jí)別機(jī)制,這是解決并發(fā)問(wèn)題的基石。

值得注意的是,在標(biāo)準(zhǔn)SQL規(guī)范中,可重復(fù)讀隔離級(jí)別是不保證解決幻讀問(wèn)題的。但MySQL的InnoDB引擎通過(guò)獨(dú)特的實(shí)現(xiàn),在可重復(fù)讀級(jí)別下也解決了幻讀問(wèn)題,這是MySQL的一個(gè)重要特性。
各隔離級(jí)別的實(shí)現(xiàn)差異
重要說(shuō)明:不同數(shù)據(jù)庫(kù)對(duì)隔離級(jí)別的實(shí)現(xiàn)存在差異。例如Oracle默認(rèn)使用讀已提交隔離級(jí)別,而MySQL默認(rèn)使用可重復(fù)讀。PostgreSQL的可重復(fù)讀級(jí)別不解決幻讀問(wèn)題,這與MySQL不同。
讓我們通過(guò)一個(gè)實(shí)際的例子來(lái)觀察不同隔離級(jí)別的行為差異:
-- 測(cè)試表結(jié)構(gòu)
CREATE TABLE account (
id INT PRIMARY KEY,
name VARCHAR(50),
balance DECIMAL(10,2),
INDEX idx_balance (balance)
);
-- 測(cè)試數(shù)據(jù)
INSERT INTO account VALUES
(1, 'Alice', 1000.00),
(2, 'Bob', 2000.00),
(3, 'Charlie', 3000.00);
在不同隔離級(jí)別下執(zhí)行以下操作序列:

在讀已提交隔離級(jí)別下,事務(wù)A的兩次查詢結(jié)果不同,出現(xiàn)了幻讀。而在可重復(fù)讀級(jí)別下,兩次查詢結(jié)果會(huì)保持一致。
三、InnoDB解決幻讀的雙重機(jī)制
現(xiàn)在我們來(lái)深入探討InnoDB引擎解決幻讀的核心機(jī)制,這是理解MySQL并發(fā)控制的關(guān)鍵。
1. 多版本并發(fā)控制(MVCC)詳解
MVCC(Multi-Version Concurrency Control)是InnoDB實(shí)現(xiàn)高并發(fā)的核心機(jī)制。它通過(guò)在每行數(shù)據(jù)后保存多個(gè)版本,使讀操作不需要等待鎖釋放,寫(xiě)操作也不需要阻塞讀操作。
InnoDB的MVCC實(shí)現(xiàn)依賴于三個(gè)關(guān)鍵字段:
- DB_TRX_ID:6字節(jié),記錄最后修改該行的事務(wù)ID
- DB_ROLL_PTR:7字節(jié),指向該行回滾段的指針(即指向歷史版本)
- DB_ROW_ID:6字節(jié),隱藏的自增行ID(當(dāng)沒(méi)有主鍵時(shí)使用)

這個(gè)類圖展示了InnoDB行數(shù)據(jù)的結(jié)構(gòu)。每次更新操作都會(huì)創(chuàng)建一個(gè)新版本,舊版本通過(guò)DB_ROLL_PTR形成版本鏈。讀操作會(huì)根據(jù)事務(wù)的ReadView決定能看到哪個(gè)版本。
ReadView的工作原理
每個(gè)事務(wù)在第一次執(zhí)行SELECT時(shí)會(huì)生成一個(gè)ReadView,包含:
- m_ids:當(dāng)前活躍的事務(wù)ID列表
- min_trx_id:m_ids中的最小值
- max_trx_id:系統(tǒng)將分配給下一個(gè)事務(wù)的ID
- creator_trx_id:創(chuàng)建該ReadView的事務(wù)ID
判斷行版本可見(jiàn)性的規(guī)則:
if (trx_id == creator_trx_id) {
// 本事務(wù)修改的,可見(jiàn)
return true;
} else if (trx_id < min_trx_id) {
// 事務(wù)已提交,可見(jiàn)
return true;
} else if (trx_id >= max_trx_id) {
// 事務(wù)還未開(kāi)始,不可見(jiàn)
return false;
} else if (trx_id in m_ids) {
// 事務(wù)未提交,不可見(jiàn)
return false;
} else {
// 事務(wù)已提交,可見(jiàn)
return true;
}
這個(gè)偽代碼展示了InnoDB如何判斷一個(gè)行版本對(duì)當(dāng)前事務(wù)是否可見(jiàn)。正是這種機(jī)制保證了可重復(fù)讀隔離級(jí)別下不會(huì)看到其他事務(wù)新插入的行。
2. 間隙鎖(Gap Lock)深度解析
**間隙鎖(Gap Lock)**是InnoDB特有的一種鎖機(jī)制,它鎖定索引記錄之間的間隙,防止其他事務(wù)在這些間隙中插入新記錄,從而解決幻讀問(wèn)題。
間隙鎖的工作范圍:

InnoDB默認(rèn)使用Next-Key鎖,它是記錄鎖和間隙鎖的組合。例如:
-- 表中存在記錄id=10,20,30 -- 事務(wù)A執(zhí)行: SELECT * FROM table WHERE id > 15 FOR UPDATE; -- 鎖定的范圍包括: -- (10,20)間隙鎖 -- 20記錄鎖 -- (20,30)間隙鎖 -- 30記錄鎖 -- (30,+∞)間隙鎖
這種鎖定方式確保了在事務(wù)A執(zhí)行期間,其他事務(wù)無(wú)法在id>15的范圍內(nèi)插入任何新記錄。
間隙鎖的觸發(fā)條件
間隙鎖主要在以下情況下觸發(fā):
- 使用
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE - UPDATE/DELETE語(yǔ)句使用索引進(jìn)行范圍條件查詢
- 事務(wù)隔離級(jí)別為可重復(fù)讀或串行化
性能注意:間隙鎖雖然解決了幻讀問(wèn)題,但會(huì)顯著降低并發(fā)性能。特別是在范圍較大的查詢時(shí),會(huì)鎖定大量間隙,導(dǎo)致其他事務(wù)長(zhǎng)時(shí)間等待。
四、完整實(shí)戰(zhàn):Java應(yīng)用中的幻讀解決方案
理解了理論后,我們通過(guò)一個(gè)完整的Java應(yīng)用示例來(lái)演示如何在實(shí)際開(kāi)發(fā)中處理幻讀問(wèn)題。
import java.sql.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PhantomReadSolution {
private static final String URL = "jdbc:mysql://localhost:3306/bank";
private static final String USER = "root";
private static final String PASSWORD = "password";
public static void main(String[] args) {
// 初始化測(cè)試數(shù)據(jù)
initTestData();
// 創(chuàng)建線程池模擬并發(fā)
ExecutorService executor = Executors.newFixedThreadPool(2);
// 事務(wù)A:檢查并更新高余額賬戶
executor.execute(() -> {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
// 設(shè)置為可重復(fù)讀隔離級(jí)別
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
conn.setAutoCommit(false);
System.out.println("【事務(wù)A】開(kāi)始,隔離級(jí)別:REPEATABLE_READ");
// 第一次查詢:獲取高余額賬戶
System.out.println("【事務(wù)A】第一次查詢:余額>1500的賬戶");
queryHighBalanceAccounts(conn);
// 模擬處理時(shí)間
Thread.sleep(2000);
// 第二次查詢:再次檢查
System.out.println("【事務(wù)A】第二次查詢:余額>1500的賬戶");
queryHighBalanceAccounts(conn);
// 執(zhí)行更新操作
System.out.println("【事務(wù)A】執(zhí)行更新:將高余額賬戶的余額增加10%");
updateHighBalanceAccounts(conn);
conn.commit();
System.out.println("【事務(wù)A】提交事務(wù)");
} catch (Exception e) {
e.printStackTrace();
}
});
// 事務(wù)B:插入新賬戶
executor.execute(() -> {
try {
// 讓事務(wù)A先開(kāi)始
Thread.sleep(500);
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
conn.setAutoCommit(false);
System.out.println("【事務(wù)B】開(kāi)始");
// 插入新賬戶
System.out.println("【事務(wù)B】插入新賬戶:David,余額1800");
PreparedStatement stmt = conn.prepareStatement(
"INSERT INTO account (name, balance) VALUES (?, ?)");
stmt.setString(1, "David");
stmt.setDouble(2, 1800.00);
stmt.executeUpdate();
conn.commit();
System.out.println("【事務(wù)B】提交事務(wù)");
}
} catch (Exception e) {
e.printStackTrace();
}
});
executor.shutdown();
}
private static void initTestData() {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
Statement stmt = conn.createStatement();
stmt.execute("DROP TABLE IF EXISTS account");
stmt.execute("CREATE TABLE account (" +
"id INT AUTO_INCREMENT PRIMARY KEY," +
"name VARCHAR(50)," +
"balance DECIMAL(10,2)," +
"INDEX idx_balance (balance))");
stmt.execute("INSERT INTO account (name, balance) VALUES " +
"('Alice', 1000.00), ('Bob', 2000.00), ('Charlie', 3000.00)");
} catch (SQLException e) {
e.printStackTrace();
}
}
private static void queryHighBalanceAccounts(Connection conn) throws SQLException {
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT id, name, balance FROM account WHERE balance > 1500");
System.out.println("高余額賬戶列表:");
while (rs.next()) {
System.out.printf("id=%d, name=%s, balance=%.2f%n",
rs.getInt("id"), rs.getString("name"), rs.getDouble("balance"));
}
rs.close();
stmt.close();
}
private static void updateHighBalanceAccounts(Connection conn) throws SQLException {
// 使用FOR UPDATE加鎖,防止幻讀影響更新操作
Statement stmt = conn.createStatement();
int count = stmt.executeUpdate(
"UPDATE account SET balance = balance * 1.1 " +
"WHERE balance > 1500");
System.out.println("更新了 " + count + " 條記錄");
stmt.close();
}
}
這個(gè)示例展示了在實(shí)際應(yīng)用中如何處理幻讀問(wèn)題:
- 使用
REPEATABLE_READ隔離級(jí)別保證一致性視圖 - 在更新操作前使用查詢鎖定相關(guān)記錄
- 通過(guò)適當(dāng)?shù)逆i機(jī)制確保更新操作不受幻讀影響
五、高級(jí)主題與最佳實(shí)踐
1. 何時(shí)會(huì)突破InnoDB的幻讀防護(hù)
雖然InnoDB的可重復(fù)讀隔離級(jí)別在大多數(shù)情況下解決了幻讀問(wèn)題,但在某些特殊場(chǎng)景下仍可能出現(xiàn)幻讀:
- 混合使用快照讀和當(dāng)前讀:同一個(gè)事務(wù)中交替使用普通SELECT和SELECT FOR UPDATE
- 使用READ COMMITTED隔離級(jí)別:此時(shí)MVCC不防止幻讀
- 沒(méi)有使用索引的查詢:會(huì)導(dǎo)致全表掃描和鎖定
特別注意:在同一個(gè)事務(wù)中混合使用快照讀和當(dāng)前讀可能導(dǎo)致邏輯上的不一致。例如:
START TRANSACTION; -- 快照讀 SELECT * FROM account WHERE balance > 1500; -- 看到2條記錄 -- 其他事務(wù)插入新記錄并提交 -- 當(dāng)前讀 SELECT * FROM account WHERE balance > 1500 FOR UPDATE; -- 看到3條記錄 -- 此時(shí)事務(wù)內(nèi)看到了"幻影行"
2. 性能優(yōu)化建議
在保證數(shù)據(jù)一致性的同時(shí),我們需要考慮性能優(yōu)化:
- 合理設(shè)計(jì)索引:間隙鎖基于索引工作,良好的索引設(shè)計(jì)可以減少鎖定范圍
- 控制事務(wù)粒度:避免長(zhǎng)時(shí)間運(yùn)行的事務(wù),減少鎖持有時(shí)間
- 慎用SELECT FOR UPDATE:只在必要時(shí)使用,考慮使用樂(lè)觀鎖替代
- 監(jiān)控鎖等待:定期檢查
SHOW ENGINE INNODB STATUS中的鎖信息
3. 替代方案:樂(lè)觀鎖實(shí)現(xiàn)
在某些場(chǎng)景下,可以使用樂(lè)觀鎖替代間隙鎖來(lái)避免幻讀:
-- 添加版本號(hào)字段
ALTER TABLE account ADD COLUMN version INT DEFAULT 0;
-- 樂(lè)觀鎖更新
UPDATE account
SET balance = balance * 1.1, version = version + 1
WHERE balance > 1500 AND version = #{oldVersion};
樂(lè)觀鎖通過(guò)版本號(hào)檢查實(shí)現(xiàn)并發(fā)控制,不會(huì)阻塞其他事務(wù),適合讀多寫(xiě)少的場(chǎng)景。
六、總結(jié)與知識(shí)體系
讓我們用思維導(dǎo)圖總結(jié)MySQL解決幻讀的完整知識(shí)體系:

關(guān)鍵要點(diǎn)回顧
- 幻讀是指在同一事務(wù)中看到新插入的行,是并發(fā)控制的核心問(wèn)題之一
- InnoDB通過(guò)MVCC和間隙鎖的組合,在REPEATABLE READ級(jí)別下解決了幻讀
- MVCC通過(guò)版本鏈和ReadView實(shí)現(xiàn)一致性讀,間隙鎖通過(guò)鎖定索引間隙防止新記錄插入
- 實(shí)際開(kāi)發(fā)中需要根據(jù)業(yè)務(wù)場(chǎng)景選擇合適的隔離級(jí)別和鎖策略
- 理解這些機(jī)制有助于設(shè)計(jì)高性能、高并發(fā)的數(shù)據(jù)庫(kù)應(yīng)用
總結(jié)
通過(guò)本文的深入探討,相信大家對(duì)MySQL如何解決幻讀問(wèn)題有了全面理解。在實(shí)際工作中,建議結(jié)合具體業(yè)務(wù)場(chǎng)景,權(quán)衡一致性和性能的需求,選擇最合適的解決方案。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
mysql如何將數(shù)據(jù)庫(kù)中的所有表結(jié)構(gòu)和數(shù)據(jù)導(dǎo)入到另一個(gè)庫(kù)
介紹了如何使用mysqldump命令備份和導(dǎo)入數(shù)據(jù)庫(kù),以及創(chuàng)建目標(biāo)數(shù)據(jù)庫(kù)的步驟,首先使用mysqldump備份源數(shù)據(jù)庫(kù),然后在目標(biāo)數(shù)據(jù)庫(kù)中創(chuàng)建數(shù)據(jù)庫(kù),并將備份文件導(dǎo)入到目標(biāo)數(shù)據(jù)庫(kù),確保數(shù)據(jù)結(jié)構(gòu)和內(nèi)容完整復(fù)制,提到了DataGrip、Navicat在導(dǎo)入導(dǎo)出過(guò)程中可能出現(xiàn)的問(wèn)題2024-10-10
MySQL數(shù)據(jù)庫(kù)聚合函數(shù)與分組查詢舉例詳解
在MySQL中聚合函數(shù)和分組查詢經(jīng)常一起使用,下面這篇文章主要給大家介紹了關(guān)于MySQL數(shù)據(jù)庫(kù)聚合函數(shù)與分組查詢的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01
MySQL超詳細(xì)實(shí)現(xiàn)用戶管理實(shí)例
MySQL 是一個(gè)多用戶數(shù)據(jù)庫(kù),具有功能強(qiáng)大的訪問(wèn)控制系統(tǒng),可以為不同用戶指定不同權(quán)限。在前面的章節(jié)中我們使用的是 root 用戶,該用戶是超級(jí)管理員,擁有所有權(quán)限,包括創(chuàng)建用戶、刪除用戶和修改用戶密碼等管理權(quán)限2022-06-06
檢查并修復(fù)mysql數(shù)據(jù)庫(kù)表的具體方法
這篇文章介紹了檢查并修復(fù)mysql數(shù)據(jù)庫(kù)表的具體方法,有需要的朋友可以參考一下2013-09-09
MySQL創(chuàng)建用戶以及用戶權(quán)限詳細(xì)圖文教程
在MySQL中可以通過(guò)創(chuàng)建用戶來(lái)管理數(shù)據(jù)庫(kù)的訪問(wèn)權(quán)限,下面這篇文章主要給大家介紹了關(guān)于MySQL創(chuàng)建用戶以及用戶權(quán)限的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2024-06-06
MySQL多表查詢的實(shí)現(xiàn)過(guò)程
文章講解了SQL多表查詢的核心概念,包括笛卡爾積(交叉連接)、等值/非等值連接、自連接、內(nèi)/外連接分類,強(qiáng)調(diào)避免笛卡爾積需添加連接條件,區(qū)分列名并使用JOIN/ON語(yǔ)法,同時(shí)對(duì)比了UNION與UNION?ALL的區(qū)別,并提及MySQL中滿外連接的替代方法2025-10-10
Mariadb數(shù)據(jù)庫(kù)的備份與恢復(fù)過(guò)程詳細(xì)介紹
MariaDB是一個(gè)流行的開(kāi)源關(guān)系型數(shù)據(jù)庫(kù)管理系統(tǒng),備份與恢復(fù)數(shù)據(jù)是數(shù)據(jù)庫(kù)管理中非常重要的一個(gè)環(huán)節(jié),能夠保證數(shù)據(jù)的安全性和可靠性,這篇文章主要介紹了Mariadb數(shù)據(jù)庫(kù)的備份與恢復(fù)過(guò)程的相關(guān)資料,需要的朋友可以參考下2025-10-10

