💱 事务管理
@Transactional 原理 · 传播行为 · 隔离级别 · 失效场景大全 · 编程式事务
1. @Transactional 的底层原理?
基于 AOP 动态代理:Spring 通过 TransactionInterceptor 拦截目标方法 → 方法执行前调用 PlatformTransactionManager.getTransaction()(内部 → AbstractPlatformTransactionManager.startTransaction() → DataSourceTransactionManager.doBegin():获取连接、关闭自动提交 autocommit=false、开启事务)→ 方法正常返回则 commit(),抛异常则 rollback() → finally 恢复连接并归还连接池。
事务三要素:
- TransactionManager:DataSourceTransactionManager(JDBC)、JpaTransactionManager、JtaTransactionManager(分布式)
- 事务定义:隔离级别、传播行为、超时、只读、回滚规则
- 连接绑定:事务期间连接存放在 ThreadLocal(TransactionSynchronizationManager),同一线程的 DAO 操作共用同一连接(否则各拿各的连接,事务无效)
🎯 面试要点
- 事务 = AOP + ThreadLocal 连接绑定:两个机制缺一不可,这是源码级理解的核心
- MyBatis 的 SqlSession 也要参与绑定:所以 Spring 集成 MyBatis 时事务要由 Spring 管理(SqlSessionTemplate 内部代理)
- 默认回滚规则:只回滚 RuntimeException 和 Error;受检异常不回滚(见失效场景)
2. 事务的传播行为(Propagation)?
传播行为:事务方法被另一个事务方法调用时,如何处理。七种,前三种常用:
- REQUIRED(默认):有事务则加入,没有则新建。内层方法异常会导致外层也回滚(同一事务)
- REQUIRES_NEW:挂起当前事务,新建独立事务。内层回滚不影响外层已执行部分。常用于:日志记录、外部接口调用失败不影响主流程
- NESTED:嵌套事务(Savepoint 实现),内层回滚只回滚到保存点,外层可捕获继续——仅 DataSourceTransactionManager 支持
- SUPPORTS(有则加入无则不用)、NOT_SUPPORTED(挂起当前事务,非事务执行)、MANDATORY(必须有事务否则异常)、NEVER(必须无事务否则异常)
经典场景:记录失败日志不污染主事务
@Service
class OrderService {
@Transactional
public void createOrder() {
try {
// 主流程...
} catch (Exception e) {
logService.saveLog(e); // REQUIRES_NEW:独立事务,主事务回滚不影响日志入库
throw e;
}
}
}
🎯 面试要点
- REQUIRES_NEW 注意:外层事务挂起期间数据库连接可能被占用更长,注意连接池大小
- NESTED 与 REQUIRES_NEW 的区别:REQUIRES_NEW 在新连接上开一个彻底独立的事务,内层提交后即使外层回滚也依然有效;NESTED 是同一事务内的保存点,内层回滚只回到保存点、外层可继续,但外层一旦回滚内层也会一起回滚
- 事务方法必须走代理调用才生效(自调用问题,见 AOP 页)
3. @Transactional 失效的场景有哪些?(必考,背全)
- 方法自调用(this 调用):不走代理 → 无事务。最常考
- 非 public 方法:Spring 只代理 public(源码检查,private/protected 直接不处理)
- 异常被 catch 吞掉:事务拦截器没看到异常 → 不回滚。必须在 catch 中抛出或手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
- 抛出受检异常:默认只回滚 RuntimeException/Error。受检异常要 rollbackFor = Exception.class
- 类未被 Spring 管理:没加 @Service/@Component 等,或者类被 final 修饰(CGLIB 无法代理)
- 数据库引擎不支持事务:MyISAM 表无事务(要用 InnoDB)
- 多线程调用:新线程的 TransactionSynchronizationManager 是独立的,事务上下文传不过去 → 新线程中的操作不受事务保护
- 事务传播配置不当:外层 NOT_SUPPORTED 等
最经典的两种
// ❌ 失效 1:异常被吞
@Transactional
public void save() {
try { dao.insert(); } catch (Exception e) { log.error(e); }
// 异常没抛出去 → 拦截器不知道 → 事务照常提交
}
// ❌ 失效 2:受检异常默认不回滚
@Transactional
public void save() throws IOException {
dao.insert();
throw new IOException("..."); // 不回滚!
}
// ✅ 修正:@Transactional(rollbackFor = Exception.class)
🎯 面试要点
- rollbackFor 一定要写:@Transactional(rollbackFor = Exception.class) 是阿里规约强制要求
- 多线程 + 事务:事务传播不了线程,需要其他方案(如本地消息表/事务消息)
- 验证事务是否生效:看日志中的 "Creating new transaction" / 或者查询 @Transactional 方法代理状态
4. 事务隔离级别与并发问题?
@Transactional(isolation = ...),对应数据库隔离级别(详见 MySQL 模块):
- READ_UNCOMMITTED(读未提交):脏读、不可重复读、幻读都有
- READ_COMMITTED(读已提交):默认(Oracle/SQL Server),防脏读
- REPEATABLE_READ(可重复读):MySQL 默认,防脏读+不可重复读;InnoDB 靠 MVCC + 间隙锁还防幻读
- SERIALIZABLE(串行化):全部防,性能最差
数据库并发三问题:脏读(读到未提交)、不可重复读(同事务两次读同一行结果不同)、幻读(同事务两次查询结果集数量不同)。
🎯 面试要点
- Spring 的 isolation 默认取数据库默认(DEFAULT)
- 只读优化:readOnly=true 只对"全只读"方法用,可减少锁开销(部分数据库有优化)
- 隔离级别设置要数据库配合(MySQL 在 InnoDB 下才有效)
5. 编程式事务 vs 声明式事务?
- 声明式(@Transactional):基于 AOP,无侵入,推荐。缺点:粒度是方法级、调试不直观
- 编程式(TransactionTemplate):手动 begin/commit/rollback,粒度可控到代码块,适合"方法内部分逻辑需要事务"的场景
TransactionTemplate 用法
@Autowired private TransactionTemplate txTemplate;
public void doBiz() {
txTemplate.execute(status -> {
try {
daoA.insert(...);
daoB.update(...);
return null; // 正常 → 提交
} catch (Exception e) {
status.setRollbackOnly(); // 标记回滚
throw e;
}
});
}
🎯 面试要点
- TransactionTemplate 默认也是只回滚 RuntimeException,注意同样配置 rollbackFor 语义(自定义 TransactionAttribute)
- 事务管理器的选择:多数据源时每个数据源一个 DataSourceTransactionManager,事务只能覆盖单个数据源(跨库要分布式事务,见分布式模块)