1. 引言
Spring 事务管理是 Java 开发中非常核心的功能,但在实际使用中,很多开发者会遇到事务不生效的情况。本文系统梳理 Spring 事务失效的常见场景,帮助你在开发中快速定位和解决问题。
2. 事务失效的常见场景
2.1 数据库引擎不支持事务
MySQL 的 MyISAM 引擎不支持事务,如果表使用 MyISAM 引擎,即使代码中配置了事务也不会生效。解决方案是使用 InnoDB 引擎。
2.2 没有被 Spring 管理
如果类没有被 Spring 容器管理(即没有标注 @Service、@Component 等注解),那么该类中的 @Transactional 注解不会生效。Spring 事务是基于 AOP 代理实现的,只有被 Spring 管理的 Bean 才能被代理。
2.3 访问权限,方法不是 public 修饰
@Transactional 注解只能用于 public 方法。如果标注在 protected、private 或包访问权限的方法上,事务不会生效。这是因为 Spring AOP 默认只对 public 方法生成代理。
2.4 方法用 final 修饰
如果 @Transactional 标注的方法被 final 修饰,事务同样不会生效。这是因为 Spring 的 AOP 代理机制------无论是 JDK 动态代理还是 CGLIB 代理------都无法覆盖 final 方法。
- JDK 动态代理 :基于接口生成代理对象,只能代理接口中定义的方法。如果方法在实现类中被
final修饰,代理对象无法重写该方法,事务增强逻辑不会被执行。 - CGLIB 代理 :通过生成目标类的子类来创建代理,子类会重写父类的非 final 方法。但
final方法不允许被子类重写,因此 CGLIB 也无法为其织入事务逻辑。
java
@Service
public class UserService {
@Transactional
public final void saveUser() {
// final 方法,事务不会生效
// 即使使用 CGLIB 代理也无法拦截
}
}
解决方案:
- 移除
final修饰符,让代理类能够正常重写方法。 - 如果方法确实需要不可变语义,考虑将事务逻辑提取到另一个非 final 的 public 方法中,由 final 方法内部调用。
- 使用编程式事务(
TransactionTemplate)在 final 方法内部手动控制事务边界。
2.5 同类内部方法调用问题
同一个类中的方法 A 调用方法 B,如果方法 B 标注了 @Transactional,事务不会生效。因为 Spring 的 AOP 代理是通过外部调用触发的,内部方法调用不会经过代理对象。
java
@Service
public class UserService {
public void methodA() {
this.methodB(); // 事务不生效
}
@Transactional
public void methodB() {
// 数据库操作
}
}
解决方案:注入自身代理,或使用 AopContext.currentProxy()。
2.6 异常类型不匹配
Spring 事务默认只回滚 RuntimeException 和 Error,对于受检异常(如 SQLException)不会回滚。如果方法抛出了受检异常但未在 @Transactional 中指定 rollbackFor,事务不会回滚。
java
@Transactional(rollbackFor = Exception.class)
public void method() throws Exception {
// 所有异常都会回滚
}
2.7 异常被捕获未抛出
如果在事务方法中 try-catch 捕获了异常但没有重新抛出,Spring 无法感知异常发生,也就不会触发回滚。
java
@Transactional
public void method() {
try {
// 数据库操作
} catch (Exception e) {
// 没有抛出异常,事务不会回滚
}
}
2.8 事务传播行为配置不当
如果使用了 Propagation.NOT_SUPPORTED 或 Propagation.NEVER,当前方法不会在事务中执行。另外,Propagation.REQUIRES_NEW 会挂起当前事务,需要注意使用场景。
2.9 多数据源事务问题
当应用配置了多个数据源时,@Transactional 默认使用主数据源的事务管理器。如果操作的是其他数据源,需要显式指定 transactionManager。
java
@Transactional(transactionManager = "secondaryTransactionManager")
public void method() {
// 操作第二个数据源
}
2.10 事务管理器未正确配置
如果没有在 Spring 配置中正确声明 PlatformTransactionManager 或 DataSourceTransactionManager,事务注解不会生效。在 Spring Boot 中,如果引入了相关 starter,通常会自动配置,但手动配置时容易遗漏。
2.11 使用了错误的代理方式
Spring 默认使用 JDK 动态代理(基于接口),如果类没有实现接口,则需要使用 CGLIB 代理。如果配置不当,可能导致事务不生效。可以通过 @EnableTransactionManagement(proxyTargetClass = true) 强制使用 CGLIB。
2.12 多线程调用场景
在多线程环境下,@Transactional 注解的事务不会跨线程传播。因为 Spring 事务默认绑定到当前线程的 ThreadLocal 中,新开启的线程无法获取到父线程的事务上下文,导致事务失效。
java
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void parentMethod() {
// 主线程中的事务
userRepository.save(new User("parent"));
new Thread(() -> {
// 新线程中事务不生效,不会与父线程共享事务
childMethod();
}).start();
}
@Transactional
public void childMethod() {
userRepository.save(new User("child"));
}
}
解决方案:
- 使用
@Async结合异步事务管理器,但需注意每个异步任务独立开启事务。 - 将多线程操作提取到独立的 Service 类中,每个线程调用独立的
@Transactional方法。 - 使用编程式事务(
TransactionTemplate)手动控制事务边界。 - 考虑使用消息队列等异步解耦方案替代多线程事务。
3. 总结
Spring 事务失效的原因可以归纳为以下几大类,每类对应不同的排查方向:
- 基础设施问题:数据库引擎不支持事务(如 MyISAM)、事务管理器未正确配置或未声明。
- 代理机制问题:同类内部方法自调用、方法非 public、方法用 final 修饰、JDK 动态代理与 CGLIB 选择不当。
- 异常处理问题 :异常类型不匹配(受检异常未指定
rollbackFor)、异常被 try-catch 捕获后未重新抛出。 - 配置问题 :事务传播行为配置不当(如
NOT_SUPPORTED、NEVER)、多数据源未指定transactionManager、多线程场景下事务不跨线程传播。
遇到事务不生效时,建议按照以上清单逐一排查,通常能快速定位问题根源。在实际开发中,建议优先检查数据库引擎和事务管理器配置,再排查代理机制和异常处理逻辑,最后审视传播行为与多线程场景,这样可以最高效地定位问题。