事务与ACID特性

理解事务的基本概念和ACID特性是构建可靠系统的基石

学习目标

掌握事务的ACID特性及其在Java中的实现原理

1

事务的定义

事务是数据库操作的最小工作单元,是不可分割的操作序列:

  • 由一组SQL语句组成
  • 必须全部成功或全部失败
  • 保证数据库从一种一致性状态转换到另一种一致性状态
2

ACID特性详解

事务必须具备的四个核心特性:

特性 描述 实现机制
原子性(Atomicity) 事务是不可分割的最小单元,要么全部成功,要么全部失败 Undo日志
一致性(Consistency) 事务执行前后,数据库状态保持一致 约束、触发器
隔离性(Isolation) 并发事务之间相互隔离,互不干扰 锁机制、MVCC
持久性(Durability) 事务提交后,对数据的修改是永久性的 Redo日志
1

开始事务

BEGIN TRANSACTION

2

执行操作

INSERT/UPDATE/DELETE

3

提交事务

COMMIT

4

回滚事务

ROLLBACK

核心概念

在分布式系统中,ACID特性尤为重要。理解事务边界(Transaction Boundary)是设计可靠系统的关键:

  • 事务应该尽可能短小,减少锁竞争
  • 避免在事务中执行远程调用等耗时操作
  • 合理设置事务超时时间

JDBC事务管理

使用原生JDBC API进行事务控制

学习目标

掌握JDBC事务的开启、提交和回滚操作

1

JDBC事务基础

JDBC通过Connection对象管理事务:

// 1. 获取数据库连接 Connection conn = DriverManager.getConnection(url, user, password); // 2. 关闭自动提交,开启事务 conn.setAutoCommit(false); try { // 3. 执行多个SQL操作 Statement stmt = conn.createStatement(); stmt.executeUpdate("UPDATE account SET balance = balance - 100 WHERE id = 1"); stmt.executeUpdate("UPDATE account SET balance = balance + 100 WHERE id = 2"); // 4. 提交事务 conn.commit(); } catch (SQLException e) { // 5. 回滚事务 conn.rollback(); } finally { // 6. 恢复自动提交并关闭连接 conn.setAutoCommit(true); conn.close(); }
2

保存点(Savepoint)

在事务中创建中间点,实现部分回滚:

conn.setAutoCommit(false); Savepoint savepoint = null; try { Statement stmt = conn.createStatement(); stmt.executeUpdate("UPDATE account SET balance = balance - 100 WHERE id = 1"); // 创建保存点 savepoint = conn.setSavepoint("SAVEPOINT_1"); stmt.executeUpdate("UPDATE account SET balance = balance + 100 WHERE id = 2"); conn.commit(); } catch (SQLException e) { if (savepoint != null) { // 回滚到保存点 conn.rollback(savepoint); conn.commit(); // 提交保存点之前的操作 } else { conn.rollback(); } }

最佳实践

  • 在finally块中关闭资源(Connection, Statement, ResultSet)
  • 使用try-with-resources自动关闭资源
  • 合理设置事务隔离级别
  • 避免在事务中持有数据库连接过长时间

Spring事务管理

使用Spring框架简化事务管理

学习目标

掌握Spring声明式事务和编程式事务的使用

1

声明式事务(@Transactional)

使用注解配置事务:

@Configuration @EnableTransactionManagement public class AppConfig { // 配置数据源和事务管理器 @Bean public DataSource dataSource() { // 创建并配置数据源 } @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dataSource()); } } @Service public class BankService { @Autowired private AccountRepository accountRepository; @Transactional( propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, timeout = 30, rollbackFor = {InsufficientBalanceException.class} ) public void transfer(Long fromId, Long toId, BigDecimal amount) { Account fromAccount = accountRepository.findById(fromId); Account toAccount = accountRepository.findById(toId); fromAccount.debit(amount); toAccount.credit(amount); accountRepository.save(fromAccount); accountRepository.save(toAccount); } }
2

编程式事务(TransactionTemplate)

通过代码显式控制事务:

@Service public class BankService { @Autowired private TransactionTemplate transactionTemplate; @Autowired private AccountRepository accountRepository; public void transfer(Long fromId, Long toId, BigDecimal amount) { transactionTemplate.execute(status -> { try { Account fromAccount = accountRepository.findById(fromId); Account toAccount = accountRepository.findById(toId); fromAccount.debit(amount); toAccount.credit(amount); accountRepository.save(fromAccount); accountRepository.save(toAccount); return true; } catch (Exception e) { status.setRollbackOnly(); throw e; } }); } }

使用建议

声明式事务 vs 编程式事务:

  • 声明式事务:更简洁,适用于大多数场景(推荐)
  • 编程式事务:更灵活,适用于复杂事务逻辑
  • 避免在同一个类中调用@Transactional方法
  • 注意事务方法中的异常处理

事务隔离级别

解决并发事务带来的问题

学习目标

理解不同隔离级别解决的问题和使用场景

读未提交(READ_UNCOMMITTED)

  • 最低隔离级别
  • 可能读取到其他事务未提交的数据(脏读)
  • 性能最高,数据一致性最差
  • 实际应用较少

读已提交(READ_COMMITTED)

  • 默认隔离级别(Oracle, PostgreSQL)
  • 解决脏读问题
  • 可能出现不可重复读
  • 使用行级锁实现

可重复读(REPEATABLE_READ)

  • MySQL默认隔离级别
  • 解决脏读和不可重复读
  • 可能出现幻读
  • 使用快照和间隙锁实现

串行化(SERIALIZABLE)

  • 最高隔离级别
  • 解决所有并发问题
  • 性能最低,完全串行执行
  • 适用于金融交易等严格要求场景
1

并发问题总结

问题 描述 解决隔离级别
脏读(Dirty Read) 读取到其他事务未提交的数据 READ_COMMITTED及以上
不可重复读(Non-repeatable Read) 同一事务中多次读取结果不同 REPEATABLE_READ及以上
幻读(Phantom Read) 同一事务中多次查询返回不同行数 SERIALIZABLE
2

设置隔离级别

在Spring中配置事务隔离级别:

// JDBC中设置隔离级别 connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // Spring声明式事务设置 @Transactional(isolation = Isolation.REPEATABLE_READ) public void updateAccount(Account account) { // 业务逻辑 }

事务传播行为

管理多个事务方法之间的调用关系

学习目标

理解不同传播行为的含义和使用场景

REQUIRED(默认)

如果当前存在事务,则加入该事务;如果不存在,则创建一个新事务。

REQUIRES_NEW

创建一个新事务,如果当前存在事务,则挂起当前事务。

SUPPORTS

如果当前存在事务,则加入该事务;如果不存在,则以非事务方式执行。

NOT_SUPPORTED

以非事务方式执行,如果当前存在事务,则挂起当前事务。

NEVER

以非事务方式执行,如果当前存在事务,则抛出异常。

MANDATORY

如果当前存在事务,则加入该事务;如果不存在,则抛出异常。

NESTED

如果当前存在事务,则创建一个嵌套事务;如果不存在,则创建一个新事务。

使用场景

  • REQUIRED:适用于大多数业务方法
  • REQUIRES_NEW:需要独立提交的日志记录操作
  • NESTED:需要部分回滚的复杂业务
  • SUPPORTS:查询操作,可参与事务但无需强制
  • 避免在事务中调用NOT_SUPPORTED或NEVER传播的方法

常见错误与解决方案

事务使用中的典型问题及解决方法

事务不生效

  • 原因:自调用问题(同个类中调用@Transactional方法)
  • 解决:使用AspectJ替代动态代理或重构代码
  • 原因:异常类型不正确(默认只回滚RuntimeException)
  • 解决:配置rollbackFor属性或抛出RuntimeException

长事务问题

  • 现象:数据库连接耗尽,系统响应缓慢
  • 原因:事务中包含远程调用、文件操作等耗时任务
  • 解决:拆分事务,将非DB操作移出事务

死锁问题

  • 现象:事务超时,数据库死锁错误
  • 原因:不同事务以不一致的顺序访问资源
  • 解决:统一资源访问顺序,使用乐观锁

幻读问题

  • 现象:范围查询结果不一致
  • 原因:REPEATABLE_READ隔离级别下仍可能发生幻读
  • 解决:升级到SERIALIZABLE或使用锁机制

调试技巧

  • 开启Spring事务调试日志:logging.level.org.springframework.transaction=DEBUG
  • 使用数据库监控工具(如MySQL的SHOW ENGINE INNODB STATUS)
  • 使用JTA事务管理器的分布式事务调试工具
  • 设置合理的事务超时时间

动手练习

通过实践巩固事务管理知识

练习目标

实现银行转账服务,处理各种事务场景

练习1:基础转账

  • 实现账户之间转账功能
  • 使用REQUIRED传播行为
  • 处理余额不足异常

练习2:事务嵌套

  • 在转账方法中调用日志记录方法
  • 日志方法使用REQUIRES_NEW传播
  • 验证转账失败时日志仍然记录

练习3:隔离级别

  • 模拟脏读、不可重复读和幻读场景
  • 调整隔离级别观察效果
  • 使用不同锁策略解决并发问题

练习4:超时与回滚

  • 设置事务超时时间
  • 模拟长时间运行事务
  • 验证超时回滚效果

扩展挑战

  • 实现分布式事务(使用Seata框架)
  • 设计补偿事务机制
  • 实现最终一致性方案
  • 使用事务同步管理器管理事务资源