Spring 是如何管理事务的:事务管理机制详解
Spring 是如何管理事务的:事务管理机制详解
面试指北 · 蔚来
题意
这道题表面在问“Spring 事务怎么用”,实际考察的是:Spring 如何把数据库、JPA、Hibernate、JTA 等不同资源的事务能力统一封装起来,并通过 AOP 把事务逻辑织入业务方法。面试官通常希望听到几条主线:
- Spring 自己不实现数据库事务,而是通过事务管理器适配底层资源;
- 声明式事务基于 AOP 代理,
@Transactional只是元数据,真正执行事务的是事务拦截器和事务管理器; - 传播行为、隔离级别、超时、只读、回滚规则分别控制什么;
- 声明式事务和编程式事务的适用场景;
- 为什么会出现“加了
@Transactional但事务没生效”。
回答时建议先给总纲,再展开核心抽象、声明式/编程式、实现原理和失效场景,避免只背七种传播行为。
结论
Spring 事务管理可以概括为一条主线:
以声明式事务为主,以 AOP 代理为入口,以 PlatformTransactionManager 为事务执行核心,以 TransactionDefinition 描述事务属性,以 TransactionStatus 记录事务状态,最终把事务的开启、提交、回滚和资源释放委托给具体资源的事务管理器。
具体来说:
@Transactional通过 AOP 代理拦截方法调用,方法执行前由事务管理器开启或加入事务,正常返回则提交,抛出异常则按回滚规则决定是否回滚;- 默认情况下,
RuntimeException和Error会回滚,受检异常Checked Exception不会回滚; - 默认传播行为是
REQUIRED,即当前有事务就加入,没有就新建; - 编程式事务通过
PlatformTransactionManager或TransactionTemplate手动控制事务边界,适合需要精细控制的场景; this自调用、方法非 public、对象未被 Spring 管理、异常被吞掉、数据库引擎不支持事务等,都会导致“事务看起来没生效”。
要点展开
1. 核心抽象:事务管理器、事务定义与事务状态
Spring 事务管理的核心接口是 PlatformTransactionManager,它只定义三个动作:
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition);
void commit(TransactionStatus status);
void rollback(TransactionStatus status);
}
围绕它还有两个重要对象:
TransactionDefinition:描述事务属性,包括传播行为、隔离级别、超时时间、是否只读等;TransactionStatus:记录当前事务状态,例如是否是新事务、是否已回滚、是否已完成等。
Spring 提供了多种事务管理器实现,常用包括:
DataSourceTransactionManager:用于管理单个关系型数据库的事务,底层基于 JDBCConnection;JpaTransactionManager:用于管理 JPA 持久化框架的事务;HibernateTransactionManager:用于管理 Hibernate 的事务;JtaTransactionManager:用于分布式事务的管理。
以 DataSourceTransactionManager 为例,它并不是“自己实现了一套事务”,而是通过 DataSourceUtils 获取连接,并借助 TransactionSynchronizationManager 把连接绑定到当前线程。这样同一个线程、同一个数据源内多次数据库操作可以复用同一个连接,从而处于同一个事务中。事务提交或回滚后,再解绑并释放资源。
2. 声明式事务:@Transactional 是如何工作的
声明式事务使用基于注解或 XML 的方式定义事务边界,Spring 推荐使用注解。
注解方式:使用 @Transactional 注解声明事务:
@Service
public class UserService {
@Transactional
public void createUser(User user) {
userRepository.save(user); // 插入操作
logRepository.saveLog("User created"); // 插入日志
}
}
- 注解可用于类或方法上。放在类上表示该类所有 public 方法默认使用该事务配置,方法上的配置可以覆盖类级别配置。
- 配置属性包括:传播行为、隔离级别、超时、只读、回滚异常类型等。
@Transactional 常见属性可以这样理解:
| 属性 | 作用 |
|---|---|
propagation |
传播行为,默认 REQUIRED |
isolation |
隔离级别,默认 DEFAULT,即交给数据库默认值 |
timeout |
超时时间,单位秒,默认 -1 表示不限制 |
readOnly |
只读提示,默认 false,不是强制禁止写 |
rollbackFor / noRollbackFor |
指定哪些异常回滚、哪些异常不回滚 |
XML 方式:
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="create*" propagation="REQUIRED" rollback-for="Exception"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.example.service.*.*(..))"/>
</aop:config>
声明式事务真正生效,依赖 @EnableTransactionManagement 注册事务基础设施,以及 TransactionInterceptor 这类事务拦截器。方法被代理后,调用会先进入拦截器:
- 根据
@Transactional元数据生成TransactionDefinition; - 调用
PlatformTransactionManager.getTransaction(...)开启或加入事务; - 执行业务方法;
- 正常返回则
commit,抛出异常则根据回滚规则决定rollback; - 最后清理事务资源。
Spring 提供两种代理方式:
- JDK 动态代理:用于代理实现了接口的类;
- CGLIB 代理:用于代理没有实现接口的类。
事务是否生效,关键不在于业务代码有没有写 @Transactional,而在于这次调用有没有经过代理对象。
3. 编程式事务:手动控制事务边界
编程式事务通过手动调用事务管理器接口实现事务管理,适合对事务边界有精确控制的场景。
示例:
@Service
public class UserService {
@Autowired
private PlatformTransactionManager transactionManager;
public void createUser(User user) {
TransactionDefinition definition = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(definition);
try {
userRepository.save(user); // 插入用户
logRepository.saveLog("User created"); // 插入日志
transactionManager.commit(status); // 提交事务
} catch (Exception e) {
transactionManager.rollback(status); // 回滚事务
throw e;
}
}
}
手写编程式事务时,要保证 commit 和 rollback 成对出现,并且回滚后通常要把异常继续抛出,避免上层误以为执行成功。
实际项目里更常用 TransactionTemplate,它把模板代码封装起来,减少漏写 commit/rollback 的风险:
@Service
public class UserService {
@Autowired
private TransactionTemplate transactionTemplate;
public void createUser(User user) {
transactionTemplate.execute(status -> {
userRepository.save(user);
logRepository.saveLog("User created");
return null;
});
}
}
声明式事务是方法级粒度,编程式事务可以做到代码块级粒度,适合批量处理分段提交、需要在方法内部动态决定事务边界等场景。
4. 事务传播行为:七个常量怎么理解
Spring 支持七种事务传播行为,由 TransactionDefinition 接口定义。传播行为解决的是:一个事务方法被另一个事务方法调用时,当前事务应该怎么处理。
| 传播行为 | 当前已有事务时 | 当前没有事务时 | 说明 |
|---|---|---|---|
PROPAGATION_REQUIRED |
加入当前事务 | 创建新事务 | 默认值,最常用 |
PROPAGATION_REQUIRES_NEW |
挂起当前事务,创建新事务 | 创建新事务 | 内外事务独立提交、回滚 |
PROPAGATION_SUPPORTS |
加入当前事务 | 以非事务方式运行 | 适合查询等可有可无事务的场景 |
PROPAGATION_NOT_SUPPORTED |
挂起当前事务 | 以非事务方式运行 | 明确不需要事务 |
PROPAGATION_NEVER |
抛出异常 | 以非事务方式运行 | 要求调用方不能有事务 |
PROPAGATION_MANDATORY |
加入当前事务 | 抛出异常 | 要求调用方必须有事务 |
PROPAGATION_NESTED |
在当前事务中创建嵌套事务,通常基于 savepoint | 行为类似 REQUIRED,创建新事务 |
回滚到保存点,不是完全独立事务 |
需要重点区分 REQUIRES_NEW 和 NESTED:
REQUIRES_NEW会挂起外层事务,开启一个独立的新事务。内层提交或回滚不影响外层,但会占用额外连接,也要注意锁竞争;NESTED是在当前事务内创建保存点,内层失败可以回滚到保存点,外层仍可选择继续或整体回滚。它依赖底层资源支持 savepoint,不是所有事务管理器都支持。
另外,传播行为只有在经过代理调用时才生效。同一个类里 this.methodB() 调用 methodB(),不会触发事务拦截,传播配置自然也不会按预期生效。
5. 隔离级别、超时、只读与回滚规则
Spring 支持的事务隔离级别包括:
ISOLATION_DEFAULT(默认):使用数据库的默认隔离级别;ISOLATION_READ_UNCOMMITTED:读未提交;ISOLATION_READ_COMMITTED:读已提交;ISOLATION_REPEATABLE_READ:可重复读;ISOLATION_SERIALIZABLE:序列化。
Spring 只是把隔离级别传递给底层数据库或持久化框架,实际支持程度取决于数据库。例如 MySQL InnoDB 默认是可重复读,Oracle 默认通常是读已提交。
超时:指定事务的最大执行时间,超过时间会尽量触发回滚。timeout 单位是秒,默认 -1 表示不限制。是否精确生效取决于底层事务管理器和资源实现,不能把它当成精确的“定时杀线程”。
只读:readOnly = true 是优化提示,可能让 JDBC 连接设置为只读,也可能让 JPA/Hibernate 调整 flush 模式,但它不是强制安全边界,不能依赖它阻止写操作。
回滚规则:默认情况下,RuntimeException 和 Error 会导致事务回滚,Checked Exception 不会回滚。如果业务需要受检异常也回滚,可以显式配置:
@Transactional(rollbackFor = Exception.class)
public void createUser(User user) {
// ...
}
同样,如果异常在事务方法内部被 try-catch 吞掉,没有继续抛出,也没有手动标记回滚,Spring 就感知不到异常,事务仍然可能提交。
6. 实现原理串联:AOP 拦截到资源释放
把前面的点串起来,声明式事务的执行链路大致是:
- 业务 Bean 被 AOP 代理;
- 调用进入
TransactionInterceptor; - 拦截器读取
@Transactional配置,构造TransactionDefinition; - 调用
PlatformTransactionManager.getTransaction(...); - 具体事务管理器根据传播行为决定加入、挂起还是新建事务;
DataSourceTransactionManager等实现把连接等资源绑定到当前线程;- 执行业务方法;
- 正常返回则提交,异常则按回滚规则回滚;
- 清理事务同步状态并释放资源。
所以,Spring 事务管理的关键词是“协调”和“委托”:AOP 负责把事务逻辑织入方法调用,事务管理器负责协调底层资源,TransactionDefinition 和 TransactionStatus 负责描述事务属性与状态。Spring 并没有替代数据库的事务能力,而是让开发者以更统一的方式使用这些能力。
7. 事务管理的配置
配置事务管理器:
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
启用事务注解:
@Configuration
@EnableTransactionManagement
public class AppConfig {
// 配置数据源和事务管理器
}
在传统 Spring 配置中,需要显式开启事务管理并声明事务管理器;Spring Boot 场景下,通常会根据数据源自动配置事务管理器,一般不需要自己手写 DataSourceTransactionManager。但理解 @EnableTransactionManagement 的作用仍然重要,它负责注册事务拦截相关基础设施,让 @Transactional 有代理可走。
易错点与对比
声明式事务与编程式事务对比
| 维度 | 声明式事务 | 编程式事务 |
|---|---|---|
| 入口 | @Transactional、XML 配置 |
PlatformTransactionManager、TransactionTemplate |
| 粒度 | 方法级 | 代码块级 |
| 优点 | 侵入小、业务代码干净 | 控制精细、边界灵活 |
| 缺点 | 自调用等场景容易失效 | 代码有侵入,需要手动处理提交回滚 |
| 适用场景 | 大多数普通业务方法 | 批量分段、动态事务边界、需要精细控制 |
常见失效与踩坑场景
- 自调用失效:
this.method()不经过代理,@Transactional不生效。解决办法是拆到另一个 Bean、注入代理对象,或使用AopContext.currentProxy(),但后两者要谨慎设计。 - 方法可见性不合适:事务方法通常应为 public。private 方法无法被常规代理拦截,final 方法也可能导致 CGLIB 无法覆盖。
- 对象不在 Spring 容器中:自己
new出来的对象没有代理,注解不会生效。 - 异常被吞掉:方法内部捕获异常但没有抛出,也没有手动标记回滚,事务不会回滚。
- 受检异常默认不回滚:需要
rollbackFor = Exception.class或类似配置。 - 多线程不共享事务:事务资源通常绑定在当前线程,子线程里的数据库操作不会自动加入父线程事务。
- 数据库引擎不支持事务:例如 MySQL 的 MyISAM 引擎不支持事务,配置再对也没用。
- 多数据源事务管理器不匹配:操作多个数据源时,要明确事务管理器,必要时使用分布式事务方案。
- 传播行为使用不当:
REQUIRES_NEW会挂起外层事务并占用新连接,可能引发连接池压力和锁竞争;NESTED依赖 savepoint