事务与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框架)
- 设计补偿事务机制
- 实现最终一致性方案
- 使用事务同步管理器管理事务资源