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.mzph.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;当所有元…