在基于Spring的项目里,给Service方法添加@Transactional注解,通常可以实现声明式事务管理。但实际开发中,方法上加了注解,甚至没有报任何异常,事务却依然没有回滚的情况并不少见。问题往往不在注解本身,而在调用方式、异常处理、类加载或数据库引擎等细节上。下面围绕八个高频场景展开分析,并给出针对性的解决思路。

这八个场景并不孤立,很多项目同时踩中其中两到三个。理解了它们背后的Spring代理和事务传播机制,排查效率会明显提升。
一、八个高频失效场景逐一拆解
1. 同类方法内部自调用
当一个类中的方法通过this调用另一个带有@Transactional的方法时,事务注解不会生效。原因是Spring事务基于AOP代理,通过容器获取的Bean才会经过代理对象,而this指向的是目标对象本身,并没有经过代理拦截。例如下面代码中,createOrder调用updateInventory时,updateInventory上的事务逻辑被绕过。
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
this.updateInventory(order); // 自调用,事务注解失效
}
@Transactional
public void updateInventory(Order order) {
inventoryMapper.decrease(order);
}
}
解决方式有两种。第一种是注入自身代理对象,把this.updateInventory(order)改成self.updateInventory(order);第二种是通过AopContext.currentProxy()获取当前代理对象再调用。推荐使用第一种,依赖关系直观,也方便单元测试。
@Service
public class OrderService {
@Autowired
private OrderService self;
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
self.updateInventory(order);
}
}
2. 目标方法不是public
Spring事务注解默认只会对public方法生效,如果方法是protected、private或者包级私有,开启注解扫描后也不会创建事务代理。Spring官方文档也明确建议将事务方法声明为public。例如下面的save方法使用private或protected修饰,注解会被忽略。
@Service
public class UserService {
@Transactional
private void save(User user) {
userMapper.insert(user);
}
}
排查时可以检查方法可见性,如果确实需要非公开方法管理事务,可以改为public,或者采用编程式事务手动控制。
3. 异常被try-catch吞掉
如果事务方法内部捕获了异常却没有继续向外抛出,事务管理器无法感知异常,只能正常提交。下面的代码在插入明细失败时被捕获,主方法仍然正常返回,最终订单主表和已插入的部分明细都会提交。
@Transactional
public void createOrder(Order order) {
try {
orderMapper.insert(order);
itemMapper.insertItems(order.getItems());
} catch (Exception e) {
log.error("插入明细失败", e); // 未抛出,事务会正常提交
}
}
正确做法是在捕获后重新抛出运行时异常,或者手动标记当前事务为回滚状态,例如调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。这样即使异常被记录,事务也会回滚。
@Transactional
public void createOrder(Order order) {
try {
orderMapper.insert(order);
itemMapper.insertItems(order.getItems());
} catch (Exception e) {
log.error("插入明细失败", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new RuntimeException(e);
}
}
4. 异常类型不匹配
Spring事务默认只在遇到RuntimeException和Error时回滚,遇到编译期异常不会回滚。很多开发者误以为只要抛出Exception就会回滚,实际上IOException、SQLException等受检异常默认不会触发回滚。
@Transactional
public void exportData() throws IOException {
orderMapper.updateState();
throw new IOException("文件写入失败"); // 默认不会回滚
}
如果业务中需要让受检异常也触发回滚,可以指定rollbackFor属性,例如@Transactional(rollbackFor = Exception.class)。也可以同时指定noRollbackFor排除某些异常,细化回滚策略。
5. 类没有被Spring容器管理
只有通过Spring容器获取的Bean,才会被事务代理包装。如果Service类没有添加@Service、@Component等注解,或者直接使用new OrderService()创建对象,方法上的事务注解没有任何效果。因为此时Spring根本没有参与对象创建,更不可能生成代理。
排查时可以先确认类是否在组件扫描路径下,以及是否存在手动new对象的情况。尤其是工具类或回调函数中,经常出现绕过容器直接创建实例的问题。
6. 数据库引擎不支持事务
Spring事务最终依赖数据库本身的回滚能力。以MySQL为例,如果表使用MyISAM引擎,即使Spring正确开启了事务,数据操作也不会回滚。MyISAM不支持事务,需要使用InnoDB等支持事务的存储引擎。
SHOW TABLE STATUS LIKE 't_order'; -- 检查Engine字段是否为InnoDB
如果表结构迁移时没有指定引擎,可能默认建成了MyISAM。此时需要将表转换为InnoDB,或者新建表时明确指定ENGINE=InnoDB。
7. 多线程或并行流中使用事务
Spring事务上下文绑定在调用线程上,事务管理器通过ThreadLocal保存连接和事务状态。一旦把数据操作放到新线程中执行,新线程并不会自动继承原线程的事务。下面的并行流操作虽然写在事务方法内,但实际执行插入的是子线程,不会纳入当前事务范围。
@Transactional
public void batchProcess(List<Order> orders) {
orders.parallelStream().forEach(order -> {
orderMapper.insert(order); // 子线程执行,事务不生效
});
}
解决思路是避免在事务方法内使用多线程,或者将每一批任务拆分到独立事务中处理,配合编程式事务控制。对于并行流,可以改为普通循环,或者单独为每个线程开启自己的事务。
8. 事务传播行为设置不当
事务传播行为控制当前方法加入或创建事务的策略。例如设置为PROPAGATION_NOT_SUPPORTED时,方法会以非事务方式执行;设置为PROPAGATION_REQUIRES_NEW时,会挂起外部事务并开启新事务,新事务的提交与回滚不会影响外部事务,也不再受外部事务回滚影响。
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createLog(OrderLog log) {
logMapper.insert(log);
}
如果外层事务回滚,但日志记录仍然希望保留,可以用REQUIRES_NEW;如果希望与外部事务一起回滚,则应使用默认的REQUIRED。错误设置传播行为可能导致方法脱离事务管理,或者出现独立的提交。
二、从Spring代理机制理解事务失效的根源
上述场景中,自调用和非public方法的问题都指向同一个核心:Spring事务依赖AOP代理实现。Spring容器启动时,会为带有事务注解的Bean生成代理对象,当调用方通过注入的Bean调用方法时,首先进入代理对象。代理对象根据方法上的事务属性决定是开启事务、加入已有事务还是直接执行。
如果方法在内部通过this调用,这个调用是目标对象的直接调用,不会经过代理链,事务增强自然无法介入。同理,非public方法通常不会被代理,因为基于接口的JDK动态代理只能代理接口方法,而CGLIB虽然可以代理继承方法,但Spring事务默认也不会处理非public方法。理解这一点后,就不难解释为什么很多事务失效场景没有任何异常日志,因为框架根本没有进入事务处理逻辑。
三、事务传播行为与多线程场景的补充说明
事务传播行为是Spring事务管理中容易被误解的一部分。REQUIRED表示当前方法需要事务,如果外层已有事务则加入,没有则新建;REQUIRES_NEW会创建一个独立的新事务,外层事务被挂起;NESTED则使用保存点实现嵌套事务。选择传播行为时,需要根据业务一致性要求来定,不能简单照搬。
多线程事务的失效主要原因是事务资源与线程绑定。Spring的事务管理器把数据库连接存放在当前线程的ThreadLocal中,新线程无法获取这份连接,所以即使方法上标注了事务,子线程的操作也不会被同一个连接覆盖。要让多线程场景正确使用事务,通常需要引入分布式事务或手动管理每个线程的事务边界。
四、实用排查技巧与经验总结
遇到事务不生效时,第一步不要急着改数据库配置,而是先确认调用链路是否经过代理。可以在Service方法内打印this.getClass().getName(),如果输出的是代理类名如OrderService$$EnhancerBySpringCGLIB,说明经过了代理;如果只是普通类名,则可能没有代理。
第二步检查事务管理的日志。开启Spring事务的DEBUG或TRACE日志,可以看到事务的创建、提交和回滚动作。如果日志里完全没有事务开启记录,通常说明方法没有被事务拦截;如果有开启但没有回滚,则要检查异常是否被吞掉或类型是否匹配。最后建议在开发环境将数据库表统一改为InnoDB,并保持事务方法尽量简单,避免多线程和自调用混用,这样能减少大多数事务失效问题。
Spring事务注解@Transactional事务失效场景修改时间:2026-09-23 08:52:38