当前位置: 首页 > 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.zskr.cn/news/1485780.html

相关文章:

  • 汉服文化网站毕设资源包:SSM后端+Vue前端,含源码、数据库、文档、演示视频与答辩材料
  • SpringBoot项目实战:用Milvus 2.0和虹软SDK,5步搞定一个简易人脸检索系统
  • 高校课程管理毕设源码包:SpringBoot后端+Vue前端+MySQL脚本+详细文档
  • MATLAB版DTW孤立词识别工程:含语音预处理、MFCC特征提取与模板匹配全流程代码
  • 三月七小助手:如何让星穹铁道的日常任务自动化帮你每天节省2小时?
  • C#版Modbus全协议通信工具包:ASCII/RTU/TCP/UDP四模一体支持
  • 星宸SSD202D芯片全解析:从硬件选型到Linux SDK上手,东山Pi开发板为何适合入门?
  • 2026大一寸证件照怎么做?尺寸规格+免费制作APP/小程序保姆教程 - 软件小管家
  • BMS设计避坑指南:BQ76PL455电压采集不准?STM32通信干扰?这些细节你注意了吗?
  • Adobe Dimension 2024深度测评
  • SpringBoot+Vue实现的应急物资管理系统源码(含论文、开题报告与数据库脚本)
  • 5个步骤彻底掌握NVIDIA显卡深度调校:从隐藏参数到性能飞跃
  • 保姆级教程:用Open3D的DBSCAN和RANSAC,5分钟搞定点云分割与聚类
  • 特征函数:连接概率论与信号处理的‘隐藏桥梁’,一个例子讲透
  • 5分钟成为硬件大师:AMD Ryzen深度调试终极指南
  • MLOps生产落地15条硬核实践:从数据版本到自动回滚
  • 2026年度漳州华起技工学校专业榜,热门推荐TOP3 - 资讯快报
  • 基于SpringBoot的轻量级企业邮件服务源码(含数据库脚本、权限管理与安全传输)
  • 2026 巴中厨卫屋面地下室漏水测评,吉修匠五星高分稳居榜首 - 苏易修缮
  • 2026年6月口碑好的高温板回收、芯片托盘回收 、ic托盘回收实力厂家推荐,专业服务贴心 - 速递信息
  • 大模型系统提示词设计原理与安全实践指南
  • 如何用GetQzonehistory永久保存QQ空间记忆:免费开源备份工具完整指南
  • 2026甘肃国际旅行社排名:专业靠谱推荐榜前三名 - 资讯快报
  • 告别盲猜!手把手教你用CANoe和ISO15031标准,精准读取车辆VIN码和校准ID($09服务实战)
  • 百度网盘直链解析:5分钟突破限速的终极解决方案
  • HALCON非常适合:
  • 2026 内江厨卫屋面地下室漏水测评,吉修匠五星高分稳居榜首 - 苏易修缮
  • 《投资-417》创业的收益、产品的性能、股票价格走势,都符合S曲线特征:低速起步→加速攀升→高位增速趋近饱和→快速衰减
  • AI 赋能传统业务:智能工单系统的工程落地与架构实践
  • 告别“黑盒”开发:用dotPeek和Symbol Server搭建你的专属源码调试环境