在日常开发中,使用 @Transactional 声明式事务已是惯例,但“加了注解却没有生效”的情况时有发生。本节梳理最常见的十种失效场景,剖析原因并给出切实可行的解决方案。
场景一:非 public 方法
现象:在 protected、private 或默认可见性的方法上添加 @Transactional,事务不生效。
原因:Spring 的事务管理基于 AOP 动态代理实现。默认 JDK 动态代理要求方法必须是 public;CGLIB 代理虽可代理非 public 方法,但 Spring 官方明确指出:@Transactional 仅对 public 方法生效。非 public 方法上的注解会被直接忽略。
示例:
@Service
public class OrderService {
@Transactional
private void innerUpdate() { // 事务失效
// ...
}
}
解决方案:将方法可见性改为 public,或将事务逻辑放到另一个 public 方法中调用。
场景二:同类内部调用(自调用)
现象:在同一个 Service 类中,一个没有事务的方法直接调用另一个有 @Transactional 的方法,事务失效。
原因:Spring 事务是通过代理对象拦截实现的。当调用同类中的方法时,实际上是 this.method(),绕过了代理对象,AOP 无法织入事务逻辑。
示例:
@Service
public class OrderService {
public void placeOrder() {
// 一些校验逻辑
this.saveOrder(); // 自调用,事务失效
}
@Transactional
public void saveOrder() {
// 持久化操作
}
}
解决方案:
- 注入自身:通过
@Autowired注入当前类的代理对象,用代理调用。
@Autowired
private OrderService self;
- 将方法拆分到不同 Service:
saveOrder()移到独立的 Service 中。 - 使用 AopContext:在启动类添加
@EnableAspectJAutoProxy(exposeProxy = true),然后通过((OrderService) AopContext.currentProxy()).saveOrder()调用。
场景三:异常被 catch 后未重新抛出
现象:事务方法内部捕获了异常但没有抛出,Spring 无法感知异常,导致事务正常提交。
原因:声明式事务通过异常触发回滚。一旦异常被 try-catch 吞掉,AOP 切面判断方法“正常返回”,于是提交事务。
示例:
@Transactional
public void updateStock() {
try {
// 扣减库存失败
stockDao.reduce();
} catch (Exception e) {
log.error("扣减失败", e);
// 没有抛出异常,事务仍会提交
}
}
解决方案:在 catch 块中明确抛出异常或手动回滚:
- 抛出
RuntimeException; - 或调用
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。
场景四:异常类型与回滚策略不匹配
现象:抛出 checked 异常(如 IOException、自定义非运行时异常),事务没有回滚。
原因:Spring 默认只对 RuntimeException 和 Error 进行回滚,对 checked 异常不触发回滚。这是符合 EJB 惯例的设计。
解决方案:在 @Transactional 中明确指定 rollbackFor。
@Transactional(rollbackFor = Exception.class)
public void processFile() throws IOException {
// 抛出 checked 异常也回滚
}
场景五:事务传播行为设置错误
现象:事务方法被另一事务方法调用时,因传播行为设置不当导致不会在事务内执行。
例如:设置了 @Transactional(propagation = Propagation.NOT_SUPPORTED),则当前事务被挂起,方法以非事务方式运行。又或者 Propagation.REQUIRES_NEW 造成独立事务,但调用者误以为仍处于同一事务。
解决方案:明确各个业务方法的实际需要,谨慎配置传播属性。默认为 REQUIRED,一般无需修改,除非有特殊场景(如日志记录独立提交)。
场景六:数据库引擎不支持事务
现象:MySQL 使用 MyISAM 引擎,@Transactional 方法执行后数据直接落盘,无法回滚。
原因:MyISAM 不支持事务控制,DDL 语句自动提交。
解决方案:将表引擎改为 InnoDB。通过 SHOW TABLE STATUS 检查,ALTER TABLE table_name ENGINE=InnoDB 修改。
场景七:多线程环境
现象:在 @Transactional 方法内部启动新线程执行数据库操作,子线程中的事务不生效或回滚异常。
原因:Spring 的事务上下文通过 ThreadLocal 绑定在当前线程,新线程无法获取原有事务。若子线程自行开启新事务,二者相互独立,回滚时无法联动。
解决方案:
- 避免在事务方法内启动新线程操作数据库;
- 如果必须异步,将事务逻辑独立于主事务之外,并辅以补偿机制(如消息队列、事件表),保证最终一致性。
场景八:方法被 final 或 static 修饰
现象:@Transactional 标注在 final 或 static 方法上,事务失效。
原因:CGLIB 代理通过生成子类实现,final 方法无法重写;JDK 动态代理依赖接口,static 方法属于类而非对象,均无法被代理增强。
解决方案:去掉 final 和 static 修饰符,保持方法可被继承覆盖。
场景九:类未纳入 Spring 容器管理
现象:在未注册为 Spring Bean 的类上使用 @Transactional,事务不生效。
原因:事务代理依赖 Spring 容器创建,自行 new 出来的对象不在容器托管范围内,@Transactional 形同虚设。
解决方案:确保类通过 @Service、@Component 等注解注册,并由 Spring 注入使用,绝不手动 new 业务组件。
场景十:未开启事务管理器或注解驱动
现象:Spring Boot 项目遗漏 @EnableTransactionManagement 或未配置 PlatformTransactionManager,导致事务完全不工作。
原因:Spring Boot 自动配置默认已引入事务管理器并开启注解驱动,但如果因自定义配置覆盖或因手动构建 Spring 环境而缺失配置,注解将无法解析。
解决方案:
- Spring Boot 项目:检查是否引入了
spring-boot-starter-data-jpa或spring-boot-starter-jdbc,它们已包含事务自动配置;若自动配置遭排除,需手动添加。 - 非 Spring Boot 项目:添加
@EnableTransactionManagement并注册DataSourceTransactionManager等管理器。
快速自查表
| 失效场景 | 核心原因 | 解决方法 |
|----------|----------|----------|
| 非 public 方法 | AOP 限制 | 改为 public |
| 同类自调用 | 绕过代理 | 注入代理或拆分 |
| 异常被吞 | 无法感知异常 | 抛出运行时异常或手动回滚 |
| Checked 异常不匹配 | 默认只回滚 RuntimeException | 指定 rollbackFor |
| 传播行为不当 | 挂起或新建事务 | 确认传播属性 |
| 数据库引擎不支持 | MyISAM 无事务 | 改用 InnoDB |
| 多线程 | 事务绑定线程 | 避免事务内多线程 |
| final/static 方法 | 无法代理 | 去掉修饰 |
| 未纳入容器 | 不是 Bean | 注册为 Spring Bean |
| 未开启事务管理 | 缺少基础配置 | 检查自动配置/手动启用 |
通过上述场景梳理,你可以在遇到事务“莫名失效”时快速定位根因,并结合解决方案及时修复。声明式事务虽简洁,但背后依赖的代理机制和异常约定仍需用心掌握。