InnoDB 如何解決幻讀:深入解析與 Java 實踐

在數據庫事務管理中,幻讀(Phantom Read)是并發操作中常見的問題,可能導致數據一致性異常。MySQL 的 InnoDB 存儲引擎通過其事務隔離機制和多版本并發控制(MVCC),有效解決了幻讀問題。作為 Java 開發者,理解 InnoDB 的幻讀解決機制不僅有助于優化數據庫操作,還能指導應用程序的事務設計。本文將深入剖析 InnoDB 如何解決幻讀,探討其底層原理,并結合 Java 代碼展示在 Spring Boot 中如何利用 InnoDB 的事務特性避免幻讀。


一、幻讀的基本概念

1. 什么是幻讀?

幻讀是指在一個事務中,多次讀取相同范圍的數據時,由于其他事務的插入操作,導致讀取到的結果集發生變化。例如:

  • 事務 A 查詢 age > 20 的用戶,得到 5 條記錄。
  • 事務 B 插入一條 age = 25 的記錄并提交。
  • 事務 A 再次查詢 age > 20,得到 6 條記錄。

這種“憑空多出”的記錄就是幻讀。幻讀不同于臟讀(未提交數據)和不可重復讀(同一行數據變化),它涉及范圍查詢的結果集變化。

2. 幻讀的影響

  • 數據一致性:報表統計、庫存檢查等場景可能因幻讀產生錯誤結果。
  • 業務邏輯:并發插入可能導致重復處理或遺漏數據。

3. 事務隔離級別與幻讀

SQL 標準定義了四種隔離級別:

  • 讀未提交(Read Uncommitted):可能出現臟讀、不可重復讀和幻讀。
  • 讀已提交(Read Committed):解決臟讀,但仍可能出現不可重復讀和幻讀。
  • 可重復讀(Repeatable Read):解決不可重復讀,InnoDB 下還能解決幻讀。
  • 串行化(Serializable):完全避免幻讀,但性能最低。

InnoDB 的默認隔離級別是可重復讀,通過 MVCC 和間隙鎖(Gap Lock)解決了幻讀問題。


二、InnoDB 解決幻讀的機制

InnoDB 結合多版本并發控制(MVCC)和鎖機制,在可重復讀隔離級別下有效防止幻讀。以下從原理和實現角度深入剖析。

1. 多版本并發控制(MVCC)

MVCC 通過維護數據的多個版本,確保事務讀取到的數據與事務開始時一致,避免其他事務的干擾。

核心概念
  • 版本號
    • 創建版本號(DB_TRX_ID):記錄創建該行的事務 ID。
    • 刪除版本號(DB_ROLL_PTR):記錄刪除該行的事務 ID(指向 Undo Log)。
  • ReadView:事務啟動時生成快照,包含活躍事務列表和當前最大事務 ID。
  • Undo Log:存儲歷史版本數據,用于回滾和快照讀取。
MVCC 解決幻讀的原理
  • 快照讀(Snapshot Read):讀取數據時,InnoDB 根據 ReadView 返回事務開始時的版本數據。
  • 規則
    1. DB_TRX_ID < ReadView.min_trx_id,數據可見(已提交)。
    2. DB_TRX_ID > ReadView.max_trx_id,數據不可見(未來數據)。
    3. DB_TRX_ID 在活躍事務列表中,數據不可見(未提交)。
  • 效果:事務 A 的范圍查詢始終基于快照,不會看到事務 B 新插入的記錄。
示例
  • 表數據:
    id | name | age | DB_TRX_ID
    1  | Alice| 25  | 100
    2  | Bob  | 30  | 100
    
  • 事務 A(ID=200)開始,生成 ReadView:min_trx_id=100, max_trx_id=200, active=[200]
  • 事務 B(ID=201)插入 id=3, age=25,提交。
  • 事務 A 查詢 age > 20,仍只看到 2 條記錄(DB_TRX_ID=201 > 200,不可見)。

2. 當前讀與間隙鎖

MVCC 僅適用于快照讀(如 SELECT),而當前讀(如 SELECT ... FOR UPDATEINSERTUPDATE)需要加鎖來解決幻讀。

當前讀的定義

當前讀讀取的是最新數據,通常涉及寫操作或顯式加鎖。

間隙鎖(Gap Lock)
  • 作用:鎖定記錄之間的“間隙”,防止其他事務插入新記錄。
  • 觸發條件:在可重復讀級別下,范圍查詢或寫操作會觸發。
  • 實現:基于 B+ 樹的索引結構,鎖定鍵值范圍。
Next-Key Lock
  • 定義:Next-Key Lock 是行鎖(Record Lock)和間隙鎖的組合,鎖定某條記錄及其前面的間隙。
  • 示例
    • 表數據:id=1, 5, 10
    • 事務 A 執行 SELECT * FROM users WHERE id > 5 FOR UPDATE
      • 鎖定 (5, 10](包含 10 和前面的間隙)。
      • 事務 B 無法插入 id=6,避免幻讀。

3. 可重復讀下的幻讀解決

  • 快照讀:MVCC 保證范圍查詢結果一致。
  • 當前讀:Next-Key Lock 防止新數據插入。
  • 串行化:通過表級鎖完全隔離,但 InnoDB 默認不使用。

三、InnoDB 解決幻讀的優缺點

1. 優點

  • 高效性:MVCC 避免了頻繁加鎖,讀操作性能高。
  • 一致性:可重復讀級別兼顧性能和隔離。
  • 靈活性:支持快照讀和當前讀,適應多種場景。

2. 缺點

  • 鎖開銷:Next-Key Lock 在高并發寫場景下可能導致死鎖。
  • 存儲成本:Undo Log 增加磁盤空間占用。
  • 復雜度:MVCC 和鎖機制實現復雜,調試困難。

四、Java 實踐:驗證 InnoDB 解決幻讀

以下通過 Spring Boot 和 MySQL,模擬幻讀場景并驗證 InnoDB 的解決方案。

1. 環境準備

  • 數據庫:MySQL 8.0(InnoDB)。
  • 表結構
CREATE TABLE users (id BIGINT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(50) NOT NULL,age INT,INDEX idx_age (age)
);INSERT INTO users (name, age) VALUES
('Alice', 25),
('Bob', 30);
  • 依賴pom.xml):
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency>
</dependencies>

2. 配置文件

spring:datasource:url: jdbc:mysql://localhost:3306/test?useSSL=falseusername: rootpassword: passworddriver-class-name: com.mysql.cj.jdbc.Driverjpa:hibernate:ddl-auto: noneproperties:hibernate:dialect: org.hibernate.dialect.MySQL8Dialectshow_sql: true

3. 實體類

@Entity
@Table(name = "users")
public class User {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String name;private Integer age;// Getters and Setterspublic Long getId() { return id; }public void setId(Long id) { this.id = id; }public String getName() { return name; }public void setName(String name) { this.name = name; }public Integer getAge() { return age; }public void setAge(Integer age) { this.age = age; }
}

4. Repository

@Repository
public interface UserRepository extends JpaRepository<User, Long> {List<User> findByAgeGreaterThan(int age);@Query("SELECT u FROM User u WHERE u.age > :age")@Lock(LockModeType.PESSIMISTIC_WRITE)List<User> findByAgeGreaterThanWithLock(@Param("age") int age);
}

5. 服務層

@Service
public class UserService {@Autowiredprivate UserRepository userRepository;@Transactional(isolation = Isolation.REPEATABLE_READ)public void testPhantomReadWithoutLock() throws InterruptedException {System.out.println("First query: " + userRepository.findByAgeGreaterThan(20).size());Thread.sleep(5000); // 模擬并發插入System.out.println("Second query: " + userRepository.findByAgeGreaterThan(20).size());}@Transactional(isolation = Isolation.REPEATABLE_READ)public void testPhantomReadWithLock() throws InterruptedException {System.out.println("First query with lock: " + userRepository.findByAgeGreaterThanWithLock(20).size());Thread.sleep(5000); // 模擬并發插入System.out.println("Second query with lock: " + userRepository.findByAgeGreaterThanWithLock(20).size());}@Transactionalpublic void insertUser(String name, int age) {User user = new User();user.setName(name);user.setAge(age);userRepository.save(user);}
}

6. 控制器

@RestController
@RequestMapping("/users")
public class UserController {@Autowiredprivate UserService userService;@GetMapping("/phantom-without-lock")public String testPhantomWithoutLock() throws InterruptedException {userService.testPhantomReadWithoutLock();return "Phantom read test without lock completed";}@GetMapping("/phantom-with-lock")public String testPhantomWithLock() throws InterruptedException {userService.testPhantomReadWithLock();return "Phantom read test with lock completed";}@PostMapping("/insert")public String insertUser(@RequestParam String name, @RequestParam int age) {userService.insertUser(name, age);return "User inserted";}
}

7. 主應用類

@SpringBootApplication
public class InnoDBDemoApplication {public static void main(String[] args) {SpringApplication.run(InnoDBDemoApplication.class, args);}
}

8. 測試場景

測試 1:快照讀(MVCC)
  • 步驟
    1. 請求:GET http://localhost:8080/users/phantom-without-lock
    2. 在 5 秒內另開終端請求:POST http://localhost:8080/users/insert?name=Charlie&age=35
  • 輸出
    First query: 2
    Second query: 2
    
  • 分析:MVCC 確保事務 A 的快照讀始終基于事務開始時的版本,事務 B 的插入不可見,避免幻讀。
測試 2:當前讀(Next-Key Lock)
  • 步驟
    1. 請求:GET http://localhost:8080/users/phantom-with-lock
    2. 在 5 秒內另開終端請求:POST http://localhost:8080/users/insert?name=David&age=40
  • 輸出
    First query with lock: 2
    Second query with lock: 2
    
  • 分析@Lock(PESSIMISTIC_WRITE) 觸發 Next-Key Lock,鎖定 age > 20 的范圍,事務 B 的插入被阻塞,直到事務 A 提交。
測試 3:驗證鎖阻塞
  • 修改插入邏輯,添加日志:
    @Transactional
    public void insertUser(String name, int age) {System.out.println("Inserting user: " + name + " at " + System.currentTimeMillis());User user = new User();user.setName(name);user.setAge(age);userRepository.save(user);System.out.println("User inserted: " + name);
    }
    
  • 步驟
    1. 請求 GET /users/phantom-with-lock
    2. 立即請求 POST /users/insert?name=Eve&age=45
  • 輸出
    First query with lock: 2
    Inserting user: Eve at 1698765432100
    Second query with lock: 2
    User inserted: Eve
    
  • 分析:插入操作被阻塞,直到查詢事務提交,證明 Next-Key Lock 生效。

五、InnoDB 解決幻讀的優化實踐

1. 索引優化

  • 為查詢字段添加索引(如 idx_age),提高鎖精度,減少范圍鎖定:
    CREATE INDEX idx_age ON users(age);
    

2. 隔離級別選擇

  • 默認使用可重復讀,必要時調整為讀已提交(允許幻讀但性能更高):
    spring:jpa:properties:hibernate:connection:isolation: 2 # READ_COMMITTED
    

3. 鎖范圍控制

  • 使用主鍵查詢替代范圍查詢,減少鎖粒度:
    userRepository.findById(id);
    

4. 性能監控

  • 啟用慢查詢日志:
    SET GLOBAL slow_query_log = 1;
    SET GLOBAL long_query_time = 1;
    
  • 檢查鎖沖突:
    SHOW ENGINE INNODB STATUS;
    

六、InnoDB 解決幻讀的源碼分析

1. MVCC 實現

InnoDB 的 row_search_mvcc 函數負責快照讀:

row_sel_t row_search_mvcc(const dict_index_t* index,const sel_node_t* node,const trx_t* trx) {if (trx->read_view.is_visible(row->trx_id)) {return ROW_FOUND;}return ROW_NOT_FOUND;
}
  • 根據 ReadView 判斷行可見性。

2. Next-Key Lock

lock_rec_lock 函數實現記錄和間隙鎖定:

void lock_rec_lock(trx_t* trx,const rec_t* rec,const dict_index_t* index) {lock_rec_add_to_queue(LOCK_REC | LOCK_GAP, rec, index, trx);
}

七、總結

InnoDB 通過 MVCC 和 Next-Key Lock 在可重復讀隔離級別下解決了幻讀問題。MVCC 保證快照讀的穩定性,Next-Key Lock 防止當前讀中的數據插入。本文從幻讀的定義入手,剖析了 InnoDB 的實現機制,并通過 Spring Boot 實踐驗證了其效果。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/901002.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/901002.shtml
英文地址,請注明出處:http://en.pswp.cn/news/901002.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【AI編程技術爆發:從輔助工具到生產力革命】

目錄 前言&#xff1a;技術背景與價值當前技術痛點解決方案概述目標讀者說明 一、技術原理剖析核心概念圖解關鍵技術模塊技術選型對比 二、實戰演示環境配置要求核心代碼實現運行結果驗證 三、性能對比測試方法論量化數據對比&#xff08;2023年數據&#xff09;結果分析 四、最…

ICRA-2025 | 視覺預測助力機器人自主導航!NavigateDiff:視覺引導的零樣本導航助理

論文&#xff1a;Yiran Qin 1 , 2 ^{1,2} 1,2, Ao Sun 2 ^{2} 2, Yuze Hong 2 ^{2} 2, Benyou Wang 2 ^{2} 2, Ruimao Zhang 1 ^{1} 1單位&#xff1a; 1 ^{1} 1中山大學&#xff0c; 2 ^{2} 2香港中文大學深圳校區論文標題&#xff1a;NavigateDiff: Visual Predictors are Ze…

【ESP32S3】GATT Server service table傳送數據到調試助手

前言 在初步學習esp32藍牙的過程中&#xff0c;借鑒了官方的GATT Server Service Table Example&#xff0c;可以在readme中看到&#xff0c;此demo是采用低功耗藍牙的通用屬性服務器來創建訂閱服務和特性。如果你接觸過MQTT&#xff0c;你會發現GATT Server這一特性和MQTT的訂…

DeepSeek :中國 AI 如何用 “小米加步槍” 逆襲硅谷

2025 年春節前夕&#xff0c;人工智能領域誕生了一項重大成果 ——DeepSeek 發布DeepSeek - R1 大模型。這一模型迅速引發廣泛關注&#xff0c;在蘋果 AppStore 中國區免費榜登頂。 DeepSeek 采用開源策略&#xff0c;依據寬松的 MIT 許可證&#xff0c;公開了模型權重、訓練方…

關稅擾動下市場波動,如何尋找確定性的長期之錨?

近期的關稅紛爭&#xff0c;擾動全球資本市場下行。A股市場一度大幅下跌。但隨著各大主力下場&#xff0c;有關部委發布有關有力措施&#xff0c;A股逐步穩住陣腳。 4月8日至4月10日&#xff0c;大盤指數連續3天上漲&#xff0c;上漲120多點&#xff0c;展現出較強的抵御關稅壁…

NeuroImage:膝關節炎如何影響大腦?靜態與動態功能網絡變化全解析

膝骨關節炎&#xff08;KOA&#xff09;是導致老年人活動受限和殘疾的主要原因之一。這種疾病不僅引起關節疼痛&#xff0c;還會顯著影響患者的生活質量。然而&#xff0c;目前對于KOA患者大腦功能網絡的異常變化及其與臨床癥狀之間的關系尚不清楚。 2024年4月10日&#xff0c;…

【KWDB 創作者計劃】KWDB 數據庫全維度解析手冊

——從原理到實踐&#xff0c;構建下一代數據基礎設施 ?第一章&#xff1a;KWDB 設計哲學與技術全景 1.1 為什么需要 KWDB&#xff1f; 在數據爆炸與業務場景碎片化的今天&#xff0c;傳統數據庫面臨三大挑戰&#xff1a;?擴展性瓶頸?&#xff08;單機性能天花板&#xff…

一個批量文件Dos2Unix程序(Microsoft Store,開源)

這個程序可以把整個目錄的文本文件改成UNIX格式&#xff0c;源碼是用C#寫的。 目錄 一、從Microsoft Store安裝 二、從github獲取源碼 三、功能介紹 3.1 運行 3.2 瀏覽 3.3 轉換 3.4 轉換&#xff08;無列表&#xff09; 3.5 取消 3.6 幫助 四、源碼解讀 五、討論和…

std::string` 類

以下是對 std::string 類中 修改操作 和 字符串操作 的示例代碼&#xff0c;幫助你更好地理解這些函數的使用&#xff1a; 5. 修改操作 (1) operator 用于追加字符串、C 風格字符串或字符。 #include <iostream> #include <string>int main() {std::string str …

《Spring Boot+策略模式:企業級度假訂單Excel導入系統的架構演進與技術實現》

前言 在數字化時代背景下&#xff0c;訂單管理系統的高效性與靈活性成為企業競爭力的核心要素。本文檔詳細剖析了一個基于 策略模式 的度假訂單導入系統&#xff0c;通過分層架構設計實現了多源異構數據的標準化處理。系統以 Spring Boot 為核心框架&#xff0c;結合 MyBatis …

SSRF漏洞公開報告分析

文章目錄 1. SSRF | 獲取元數據 | 賬戶接管2. AppStore | 版本上傳表單 | Blind SSRF3. HOST SSRF一、為什么HOST修改不會影響正常訪問二、案例 4. Turbonomic 的 終端節點 | SSRF 獲取元密鑰一、介紹二、漏洞分析 5. POST | Blind SSRF6. CVE-2024-40898利用 | SSRF 泄露 NTL…

告別 ifconfig:為什么現代 Linux 系統推薦使用 ip 命令

告別 ifconfig&#xff1a;為什么現代 Linux 系統推薦使用 ip 命令 ifconfig 指令已經被視為過時的工具&#xff0c;不再是查看和配置網絡接口的推薦方式。 與 netstat 被 ss 替代類似。 本文簡要介紹 ip addr 命令的使用 簡介ip ifconfig 屬于 net-tools 包&#xff0c;這個…

VLC快速制作rtsp流媒體服務器

1.安裝vlc media player工具 2.打開后點擊菜單 媒體->流 3.添加mp4視頻&#xff0c;選擇串流 4.選擇 下一個 5.新目標選擇 RTSP&#xff0c;點擊添加按鈕 6.端口和路徑隨便填寫&#xff0c;如果推流失敗就換個端口。一路操作下去 7.點擊 流 按鈕后&#xff0c;就可以看到下圖…

基于 JavaWeb 的 SSM 在線視頻教育系統設計和實現(源碼+文檔+部署講解)

技術范圍&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬蟲、數據可視化、小程序、安卓app、大數據、物聯網、機器學習等設計與開發。 主要內容&#xff1a;免費功能設計、開題報告、任務書、中期檢查PPT、系統功能實現、代碼編寫、論文編寫和輔導、論文…

RK3568 基于Gstreamer的多媒體調試記錄

文章目錄 1、環境介紹2、概念理清3、提前準備4、GStreamer編譯5、GStreamer基礎介紹6、視頻播放初體驗7、視頻硬編碼7.1、h2647.2、h265 8、視頻硬解碼8.1、解碼視頻并播放解碼視頻并播放帶音頻 1、環境介紹 硬件&#xff1a;飛凌ok3568-c開發板 軟件&#xff1a;原廠rk356x …

Mac學習使用全借鑒模式

Reference https://zhuanlan.zhihu.com/p/923417581.快捷鍵 macOS 的快捷鍵組合很多&#xff0c;相應的修飾鍵就多達 6 個&#xff08;Windows 系統級就 4 個&#xff09;&#xff1a; Command ? Shift ? Option ? Control ? Caps Lock ? Fn 全屏/退出全屏 command con…

SpringBoot多線程,保證各個子線程和主線程事物一致性

SpringBoot多線程&#xff0c;保證各個子線程和主線程事物一致性 1、第一種寫法1.1、TransactionalUntil工具類1.2、service業務類 2、第二種寫法2.1、service業務類 1、第一種寫法 1.1、TransactionalUntil工具類 import org.springframework.jdbc.datasource.DataSourceTra…

高并發的業務場景下,如何防止數據庫事務死鎖

一、 一致的鎖定順序 定義: 死鎖的常見原因之一是不同的事務以不同的順序獲取鎖。當多個事務獲取了不同資源的鎖,并且這些資源之間發生了互相依賴,就會形成死鎖。 解決方法: 確保所有的事務在獲取多個鎖時,按照相同的順序請求鎖。例如,如果事務A需要鎖定表A和表B,事務…

【從0到1學MybatisPlus】MybatisPlus入門

Mybatis-Plus 使用場景 大家在日常開發中應該能發現&#xff0c;單表的CRUD功能代碼重復度很高&#xff0c;也沒有什么難度。而這部分代碼量往往比較大&#xff0c;開發起來比較費時。 因此&#xff0c;目前企業中都會使用一些組件來簡化或省略單表的CRUD開發工作。目前在國…

力扣HOT100之鏈表: 148. 排序鏈表

這道題直接用蠢辦法來做的&#xff0c;直接先遍歷一遍鏈表&#xff0c;用一個哈希表統計每個值出現的次數&#xff0c;由于std::map<int, int>會根據鍵進行升序排序&#xff0c;因此我們將節點的值作為鍵&#xff0c;其在整個鏈表中的出現次數作為值&#xff0c;當所有元…