当前位置: 首页 > news >正文

别再死记硬背了!用Spring Boot + MySQL实战演示四种隔离级别下的数据‘错乱’现场

用Spring Boot + MySQL实战解析四种隔离级别的数据异常现象

在数据库开发中,事务隔离级别是一个既基础又关键的概念。很多开发者虽然能背出四种隔离级别的定义,却对它们在实际应用中的表现缺乏直观感受。本文将带你通过Spring Boot应用和MySQL数据库,亲手"制造"并观察不同隔离级别下的数据异常现象。

1. 环境准备与项目搭建

首先创建一个基础的Spring Boot项目,添加必要的依赖。我们将使用Spring Data JPA与MySQL进行交互,同时用JUnit编写测试用例来模拟并发事务场景。

<!-- pom.xml关键依赖 --> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>

配置application.properties文件,设置数据库连接和JPA属性:

spring.datasource.url=jdbc:mysql://localhost:3306/transaction_demo spring.datasource.username=root spring.datasource.password=yourpassword spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true

创建一个简单的实体类用于测试:

@Entity public class Account { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private BigDecimal balance; // 省略getter和setter }

2. 理解隔离级别与数据异常

在深入代码前,我们需要明确几个关键概念:

  • 脏读(Dirty Read):一个事务读取了另一个未提交事务修改过的数据
  • 不可重复读(Non-repeatable Read):同一事务内,多次读取同一数据返回不同结果
  • 幻读(Phantom Read):同一事务内,相同的查询条件返回不同数量的记录

MySQL支持的四种隔离级别及其可能发生的问题:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED
READ COMMITTED×
REPEATABLE READ××
SERIALIZABLE×××

3. 实战演示四种隔离级别

3.1 READ UNCOMMITTED下的脏读现象

在这个隔离级别下,我们将观察到最"宽松"的数据异常现象。

@Test @Transactional(isolation = Isolation.READ_UNCOMMITTED) public void testDirtyRead() throws InterruptedException { // 事务1:修改数据但不提交 new Thread(() -> { transactionTemplate.execute(status -> { Account account = accountRepository.findById(1L).orElseThrow(); account.setBalance(new BigDecimal("1000.00")); accountRepository.save(account); // 故意不提交,模拟长时间运行的事务 try { Thread.sleep(5000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return null; }); }).start(); // 事务2:在事务1提交前读取数据 Thread.sleep(1000); // 确保事务1已经开始 Account account = accountRepository.findById(1L).orElseThrow(); System.out.println("读取到未提交的数据:" + account.getBalance()); }

运行这个测试,你会看到事务2读取到了事务1尚未提交的修改,这就是典型的脏读现象。

3.2 READ COMMITTED下的不可重复读

将隔离级别提高到READ COMMITTED,脏读问题解决了,但会出现不可重复读。

@Test public void testNonRepeatableRead() throws InterruptedException { // 初始数据 Account account = new Account(); account.setName("测试账户"); account.setBalance(new BigDecimal("500.00")); accountRepository.save(account); // 事务1:读取数据 CompletableFuture<BigDecimal> firstRead = CompletableFuture.supplyAsync(() -> transactionTemplate.execute(status -> { Account acc = accountRepository.findById(account.getId()).orElseThrow(); return acc.getBalance(); }) ); // 事务2:修改并提交数据 CompletableFuture.runAsync(() -> transactionTemplate.execute(status -> { Account acc = accountRepository.findById(account.getId()).orElseThrow(); acc.setBalance(new BigDecimal("1000.00")); accountRepository.save(acc); return null; }) ); // 事务1:再次读取 CompletableFuture<BigDecimal> secondRead = CompletableFuture.supplyAsync(() -> transactionTemplate.execute(status -> { Account acc = accountRepository.findById(account.getId()).orElseThrow(); return acc.getBalance(); }) ); BigDecimal first = firstRead.get(); secondRead.get(); // 等待事务2完成 BigDecimal second = secondRead.get(); System.out.println("第一次读取:" + first); System.out.println("第二次读取:" + second); }

在这个测试中,尽管是在同一个事务内,两次读取的结果却不一致,这就是不可重复读。

3.3 REPEATABLE READ下的幻读现象

MySQL的默认隔离级别是REPEATABLE READ,它解决了不可重复读问题,但仍可能出现幻读。

@Test public void testPhantomRead() throws InterruptedException { // 事务1:查询符合条件的记录数 CompletableFuture<Long> firstCount = CompletableFuture.supplyAsync(() -> transactionTemplate.execute(status -> accountRepository.countByName("测试账户") ) ); // 事务2:插入新记录并提交 CompletableFuture.runAsync(() -> transactionTemplate.execute(status -> { Account newAccount = new Account(); newAccount.setName("测试账户"); newAccount.setBalance(new BigDecimal("200.00")); accountRepository.save(newAccount); return null; }) ); // 事务1:再次查询 CompletableFuture<Long> secondCount = CompletableFuture.supplyAsync(() -> transactionTemplate.execute(status -> accountRepository.countByName("测试账户") ) ); Long first = firstCount.get(); secondCount.get(); // 等待事务2完成 Long second = secondCount.get(); System.out.println("第一次计数:" + first); System.out.println("第二次计数:" + second); }

在REPEATABLE READ隔离级别下,同一个事务内两次查询返回的记录数可能不同,这就是幻读现象。

3.4 SERIALIZABLE隔离级别的行为

最高级别的隔离级别SERIALIZABLE解决了所有数据异常问题,但会带来性能开销。

@Test @Transactional(isolation = Isolation.SERIALIZABLE) public void testSerializable() { // 事务1:查询并锁定记录 List<Account> accounts = accountRepository.findByName("测试账户"); // 事务2尝试修改被锁定的记录 CompletableFuture<Void> future = CompletableFuture.runAsync(() -> transactionTemplate.execute(status -> { Account account = accountRepository.findById(1L).orElseThrow(); account.setBalance(new BigDecimal("1500.00")); accountRepository.save(account); return null; }) ); try { future.get(2, TimeUnit.SECONDS); } catch (Exception e) { System.out.println("事务2被阻塞或超时"); } }

在这个测试中,你会观察到事务2会被阻塞,直到事务1完成。这是SERIALIZABLE隔离级别的典型行为。

4. 隔离级别的选择与实践建议

在实际开发中,隔离级别的选择需要权衡数据一致性和系统性能:

  • 低一致性要求场景:如日志记录、统计分析等,可以考虑READ COMMITTED
  • 一般业务场景:MySQL默认的REPEATABLE READ通常是最佳选择
  • 高一致性要求场景:如金融交易,可能需要使用SERIALIZABLE

几个实用技巧:

  1. 在Spring中设置隔离级别:
@Transactional(isolation = Isolation.REPEATABLE_READ) public void businessMethod() { // 业务逻辑 }
  1. 监控数据库性能,当发现锁争用严重时,考虑调整隔离级别

  2. 对于特定操作,可以使用SELECT ... FOR UPDATE显式加锁

  3. 合理设计事务边界,避免长事务

在实际项目中,我遇到过因不当使用SERIALIZABLE隔离级别导致的性能问题。通过将这些演示代码应用到真实场景,你能更直观地理解不同隔离级别的行为特征,从而做出更合理的技术决策。

http://www.jsqmd.com/news/974057/

相关文章:

  • SpringBoot项目实战:用Milvus 2.0和虹软SDK,5步搞定一个简易人脸检索系统
  • 汉服文化网站毕设资源包:SSM后端+Vue前端,含源码、数据库、文档、演示视频与答辩材料
  • 高校课程管理毕设源码包:SpringBoot后端+Vue前端+MySQL脚本+详细文档
  • PHP简单工厂与抽象工厂对比
  • MATLAB版DTW孤立词识别工程:含语音预处理、MFCC特征提取与模板匹配全流程代码
  • 三月七小助手:如何让星穹铁道的日常任务自动化帮你每天节省2小时?
  • 2026大一寸证件照怎么做?尺寸规格+免费制作APP/小程序保姆教程 - 软件小管家
  • 卫星语义通信中的特征敏感排序技术解析
  • 点云数据里一键抠出平面、圆柱、长方体等常见3D形状的Python小工具
  • 从环境变量到源码:彻底搞懂QML模块导入失败的那些坑
  • 星宸SSD202D芯片全解析:从硬件选型到Linux SDK上手,东山Pi开发板为何适合入门?
  • C#版Modbus全协议通信工具包:ASCII/RTU/TCP/UDP四模一体支持
  • STM32F103R6在Proteus里跑PWM和正弦波输出的完整仿真工程包(含Keil项目+HEX固件)
  • 别再乱写注释了!手把手教你用Doxygen生成专业API文档(附常用标记速查表)
  • OpenFPGA环境搭建踩坑实录:从GTK3到TBB,手把手解决编译中的5个常见报错
  • 魔兽争霸III全面优化指南:Warcraft Helper让你的经典游戏焕发新生
  • 从银行U盾到手机APP:聊聊HOTP/TOTP那些年我们踩过的‘坑’与最佳实践
  • BMS设计避坑指南:BQ76PL455电压采集不准?STM32通信干扰?这些细节你注意了吗?
  • SpringBoot+Vue实现的应急物资管理系统源码(含论文、开题报告与数据库脚本)
  • Adobe Dimension 2024深度测评
  • 2026合肥免砸砖漏水维修全攻略|卫生间/阳台/厨房/屋顶根治方法+避坑指南|苏易修缮 - 苏易修缮
  • 临安母婴除甲醛CMA甲醛检测治理公司深度测评:绿呼吸环保稳居榜首 - 一休咨询
  • C#写的实时运动检测小工具:接摄像头或视频文件,画框标出移动物体(VS工程直接编译运行)
  • 2026沈阳市权威认证贵金属回收 TOP5+黄金回收白银回收铂金回收门店地址电话推荐
  • 特征函数:连接概率论与信号处理的‘隐藏桥梁’,一个例子讲透
  • 为什么选择appserver.io?PHP应用服务器性能提升10倍的终极指南 [特殊字符]
  • 5个步骤彻底掌握NVIDIA显卡深度调校:从隐藏参数到性能飞跃
  • 传统拉肚子就要禁食,编写程序结合腹泻程度,电解质数据,判定是否需要进食,推荐温和食材。
  • 保姆级教程:用Open3D的DBSCAN和RANSAC,5分钟搞定点云分割与聚类
  • 5分钟成为硬件大师:AMD Ryzen深度调试终极指南