@Transactional 注解详解与代理对象问题解析
Spring 的 @Transactional 注解用于声明式事务管理,它通过 AOP 代理 在方法执行前后添加事务控制逻辑。正确理解其原理和常见陷阱是避免事务失效的关键。
1. 核心工作原理
- 代理生成 :Spring 容器启动时,会扫描
@Transactional注解,为对应的 Bean 创建代理对象(JDK 动态代理或 CGLIB)。 - 方法拦截:当外部调用 Bean 的方法时,实际调用的是代理对象的方法。代理对象先执行事务相关操作(如开启事务、设置隔离级别),再调用目标对象的原方法,最后根据执行结果提交或回滚事务。
- 事务绑定 :事务与当前线程绑定(通过
ThreadLocal存储事务资源),因此同一线程内的方法调用默认共享同一个事务。
2. 关键属性详解
java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Transactional {
// 事务管理器名称,用于指定使用哪个事务管理器(多数据源时常用)
String value() default "";
String transactionManager() default "";
// 传播行为(默认 REQUIRED)
Propagation propagation() default Propagation.REQUIRED;
// 隔离级别(默认使用数据库默认隔离级别)
Isolation isolation() default Isolation.DEFAULT;
// 超时时间(秒),默认-1表示不超时
int timeout() default -1;
int timeoutString() default -1;
// 只读事务,优化性能,适用于查询
boolean readOnly() default false;
// 哪些异常导致回滚(默认仅回滚 RuntimeException 和 Error)
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
// 哪些异常不回滚(即使它是 RuntimeException)
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
2.1 传播行为(Propagation)
REQUIRED(默认):支持当前事务,如果不存在则新建。SUPPORTS:支持当前事务,如果不存在则非事务执行。MANDATORY:支持当前事务,如果不存在则抛出异常。REQUIRES_NEW:新建事务,挂起当前事务(如果存在)。NOT_SUPPORTED:非事务执行,如果存在事务则挂起。NEVER:非事务执行,如果存在事务则抛出异常。NESTED:如果当前存在事务,则在该事务内嵌套一个子事务(保存点);否则行为同REQUIRED。
2.2 隔离级别(Isolation)
DEFAULT:使用数据库默认隔离级别。READ_UNCOMMITTED:读未提交(可能导致脏读、不可重复读、幻读)。READ_COMMITTED:读已提交(避免脏读,Oracle默认)。REPEATABLE_READ:可重复读(避免脏读和不可重复读,MySQL默认)。SERIALIZABLE:串行化(最高隔离级别,性能低)。
2.3 只读事务
readOnly = true:当数据库支持时,会优化查询性能,也会禁止数据修改操作(如INSERT/UPDATE)。
2.4 回滚规则
- 默认只对
RuntimeException及其子类 和Error进行回滚,检查型异常(Exception的子类且非RuntimeException)不会自动回滚。 - 可通过
rollbackFor指定需要回滚的异常类型,noRollbackFor指定不回滚的异常类型。
3. 代理对象错误详解:为什么事务会失效?
Spring 事务的生效依赖于代理对象。如果方法调用没有经过代理对象,那么 @Transactional 注解就不会被解析,事务逻辑不会执行。以下是常见的"代理对象错误"导致事务失效的场景。
3.1 自调用(内部方法调用)
java
@Service
public class UserService {
@Transactional
public void addUser(User user) {
// 数据库操作
updateUser(user); // 直接调用内部方法,绕过了代理!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateUser(User user) {
// 数据库操作
}
}
当外部调用 userService.addUser() 时,通过代理对象调用,addUser 的事务生效。但 addUser 内部直接通过 this.updateUser() 调用,相当于调用了目标对象的原始方法,没有经过代理,因此 updateUser 上的 @Transactional(REQUIRES_NEW) 被忽略,它和 addUser 使用同一个事务,或者根本不开启新事务。
3.2 方法非 public
Spring 默认只代理 public 方法(即使使用 CGLIB,默认也只拦截 public 方法)。如果 @Transactional 标注在 private、protected 或默认访问权限的方法上,事务不会生效。
java
@Transactional
private void update() { // 事务不生效
// ...
}
3.3 类或方法被 final 修饰
CGLIB 通过生成子类来实现代理,如果目标类或方法被 final 修饰,则无法继承和重写,代理失效。JDK 动态代理基于接口,如果类没有实现接口且未被 final 修饰,则 Spring 使用 CGLIB。
3.4 异常被捕获后未抛出
java
@Transactional
public void method() {
try {
// 可能抛出异常的操作
} catch (Exception e) {
// 仅打印日志,未抛出异常
log.error("error", e);
// 事务不会回滚,因为异常被吞没了
}
}
事务管理器只能感知到从方法抛出的异常,如果异常被捕获且未重新抛出,则事务正常提交。
3.5 异常类型不匹配
java
@Transactional // 默认只回滚 RuntimeException
public void method() throws IOException {
// 抛出检查型异常 IOException
throw new IOException();
}
由于 IOException 不是 RuntimeException,事务不会回滚。需要指定 rollbackFor = IOException.class。
3.6 多数据源时未指定正确的事务管理器
java
@Transactional("primaryTxManager")
public void method() { ... }
如果未指定 value 或 transactionManager,则使用默认的事务管理器。在多数据源场景下可能导致事务未绑定到正确的数据源。
3.7 数据库引擎不支持事务
如 MySQL 的 MyISAM 引擎不支持事务,即使代码正确也无法回滚。
3.8 代理方式不正确
如果 Bean 实现了接口,Spring 默认使用 JDK 动态代理。此时如果目标类中的方法未在接口中定义,则无法被代理(但可通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用 CGLIB)。
3.9 在非 Spring 管理的对象中使用
如果对象不是由 Spring 容器管理(比如手动 new 出来的),那么即使标注了 @Transactional,Spring 也不会为其创建代理,事务自然无效。
4. 解决方案:确保事务通过代理对象执行
4.1 注入自身代理(推荐)
java
@Service
public class UserService {
@Autowired
private UserService self; // 注入自身代理
@Transactional
public void addUser(User user) {
// ...
self.updateUser(user); // 通过代理调用,事务生效
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateUser(User user) {
// ...
}
}
注意:Spring 可以处理这种自注入,但需避免循环依赖,通常字段注入没问题。
4.2 使用 AopContext.currentProxy()
java
@EnableAspectJAutoProxy(exposeProxy = true) // 在配置类中开启
@Service
public class UserService {
@Transactional
public void addUser(User user) {
((UserService) AopContext.currentProxy()).updateUser(user);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateUser(User user) {
// ...
}
}
这种方式代码耦合度较高,且需要开启 exposeProxy。
4.3 将事务方法拆分到不同的 Bean
java
@Service
public class UserService {
@Autowired
private UserUpdateService updateService;
@Transactional
public void addUser(User user) {
updateService.updateUser(user);
}
}
@Service
public class UserUpdateService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateUser(User user) {
// ...
}
}
这样调用自然经过代理,清晰且易于测试。
4.4 使用编程式事务
如果不想依赖代理,可以直接使用 TransactionTemplate 或 PlatformTransactionManager 手动控制事务。
5. 注意事项与最佳实践
- 首选
public方法标注注解 ,避免放在private方法上。 - 避免内部方法自调用,通过注入自身代理或拆分 Bean 解决。
- 明确回滚规则 :如果需要回滚检查型异常,使用
rollbackFor指定。 - 合理选择传播行为 :
REQUIRES_NEW和NESTED会创建新事务或保存点,注意资源消耗。 - 只读事务优化 :对于纯查询方法,使用
readOnly = true提升性能。 - 多数据源:确保每个事务指定正确的事务管理器。
- 测试事务是否生效 :在单元测试中故意抛出异常,检查数据是否回滚;或打印
this.getClass()确认是否为代理类。 - 注意线程边界:事务与线程绑定,新开启的线程不会继承原有事务。
- 代理方式选择 :如果希望事务在类内非接口方法上生效,应强制使用 CGLIB(
spring.aop.proxy-target-class=true或@EnableAspectJAutoProxy(proxyTargetClass = true))。
6. 总结
@Transactional 是 Spring 提供的强大事务管理工具,但其底层依赖于 AOP 代理。要确保事务正确生效,必须保证调用链路上所有标注了 @Transactional 的方法都通过 Spring 代理对象执行。最常见的失效场景是 内部方法自调用 、非 public 方法 以及 异常被吞没 。通过注入自身代理、拆分 Bean 或使用 AopContext.currentProxy() 可以解决自调用问题。理解代理机制和事务属性,才能在复杂业务中灵活应用
事务失效原理剖析
Spring 声明式事务的核心是 AOP 代理:通过代理对象拦截目标方法,在方法执行前后添加事务管理逻辑(开启、提交、回滚)。事务管理器与当前线程绑定(TransactionSynchronizationManager),并通过异常类型决定提交或回滚。失效的根本原因在于 调用链未能经过代理对象 或 事务管理器无法正确感知异常/数据源。
1. 自调用(内部方法调用)
- 原理 :外部调用时,调用的是代理对象的方法,代理对象会添加事务逻辑。但类内部通过
this调用另一个方法时,this指向原始对象而非代理对象,因此被调方法上的@Transactional不会触发任何拦截,事务逻辑不生效。
2. 非 public 方法
- 原理 :Spring 事务拦截器
AbstractFallbackTransactionAttributeSource默认只对public方法解析事务注解。即使 CGLIB 能代理非 public 方法,Spring 也出于安全设计默认不处理。代理对象无法在非 public 方法上织入事务增强。
3. 异常被吞没
- 原理 :事务管理器通过方法执行抛出的异常决定回滚。若异常被
try-catch捕获且未重新抛出,方法正常返回,事务管理器认为执行成功,执行commit。回滚的触发依赖于异常沿调用栈传播到代理方法。
4. 异常类型不匹配
- 原理 :
@Transactional默认回滚规则为RuntimeException和Error(基于RuleBasedTransactionAttribute)。检查型异常(如IOException)默认不回滚,因为 Spring 认为这类异常可能属于业务预期结果,除非通过rollbackFor显式指定。
5. 数据库引擎不支持事务
- 原理 :Spring 只负责发出
begin、commit、rollback命令,事务的原子性由底层数据库引擎保证。若引擎不支持事务(如 MyISAM),这些命令会被忽略或无效,数据修改即时生效,无法回滚。
6. final 类或方法
- 原理 :Spring 默认使用 CGLIB 生成代理子类,通过继承目标类并重写方法实现拦截。若类或方法被
final修饰,则无法被继承或重写,代理类无法插入事务拦截逻辑。
7. 非 Spring 管理的 Bean
- 原理 :Spring 仅在容器管理的 Bean 上扫描
@Transactional并生成代理。手动new的对象不属于 IoC 容器,没有代理过程,注解完全被忽略。
8. 多数据源未指定事务管理器
- 原理 :当存在多个
PlatformTransactionManager时,Spring 需要明确知道事务绑定到哪个数据源。若未通过@Transactional("managerName")指定,默认会选择第一个注册的事务管理器,可能导致事务操作错误的数据源,出现数据不一致。
9. 传播行为使用不当
- 原理 :传播行为控制事务边界。例如
SUPPORTS:若当前无事务则不开启事务,方法以非事务方式运行;NOT_SUPPORTED:挂起当前事务,方法非事务执行。这些传播行为会导致方法没有事务上下文,注解形同虚设。
10. 代理方式限制(JDK 动态代理)
- 原理:JDK 动态代理要求目标类实现接口,且代理对象仅能拦截接口中声明的方法。若事务方法仅在实现类中定义而未在接口中声明,则调用该方法时不会经过代理对象,事务失效。
核心结论 :事务生效的必要条件是 目标方法通过 Spring 代理对象调用,且事务管理器能正确识别异常和数据源。任何破坏这个条件的因素(调用路径绕过代理、注解位置不当、异常处理失当、底层基础设施缺失等)都会导致事务失效。。