Transaction失效的场景
避坑指南:为什么你的@Transactional突然不工作了?
日常开发中,我们绝大多数情况下都是使用 @Transactional 注解来开启和管理事务的。它通过声明式的方式,极大地简化了事务编程,让我们能从繁琐的 try-catch-finally 和手动 commit/rollback 中解放出来,专注于业务逻辑。这正是Spring AOP(面向切面编程)的魅力所在。
然而,正是因为 @Transactional 如此方便,我们有时会忽略其背后的工作原理,从而在不经意间踩到一些“坑”,导致事务“悄无声息”地失效了。
今天,我们来聊一个老生常谈但又总有人踩坑的话题——Spring的 @Transactional 事务。
@Transactional 注解就像一把瑞士军刀,轻巧、方便、功能强大。我们只需要在方法上轻轻一标,就能享受到事务带来的数据一致性保障。但你是否遇到过这样的场景:明明加了注解,代码也看似没问题,可数据库里的数据却“我行我素”,事务压根没生效?
别慌,这通常不是Spring的Bug,而是我们不小心绕过了它的“游戏规则”。下面,我们就来盘点一下导致事务失效的五大元凶,帮你彻底搞懂它,告别踩坑!
背后原理速览:AOP代理
在开始之前,我们必须先理解 @Transactional 的核心魔法:AOP代理。
当你为一个Bean(通常是 @Service 注解的类)的public方法标注了 @Transactional 后,Spring并不会直接把这个Bean给你,而是会为它创建一个代理对象。当你调用这个方法时,实际上是调用了代理对象的方法。这个代理对象就像一个“保安”,它会在你的业务方法执行前,开启事务;在方法执行后,根据执行情况(是否抛出异常)来决定是提交(Commit)还是回滚(Rollback)事务。
核心:事务的生效与否,关键在于你是否通过代理对象调用了方法。 记住这一点,我们就能轻松理解下面的所有失效场景。
元凶一:方法内部调用(自调用)
这是最常见也最隐蔽的失效场景。
场景描述: 在同一个类中,一个没有事务注解的方法A,调用了另一个有事务注解的方法B。
失效原因: 当你调用方法A时,你操作的是真实的this对象,而不是Spring的代理对象。因此,this.方法B() 的调用,本质上是对象内部的普通方法调用,完全绕过了代理对象的“保安”,AOP切面自然无法介入,事务也就失效了。
错误示例:
1 |
|
在上面的例子中,即使 insertOrder 执行时抛出异常,数据库中的订单数据也不会回滚。
解决方案:
让调用方从 this 变成 代理对象。
-
【推荐】注入自己,通过代理对象调用:
在类中注入自身的代理对象,然后用这个代理对象去调用事务方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class OrderServiceImpl implements OrderService {
private OrderMapper orderMapper;
// 注入自己的代理对象
// @Lazy可以解决循环依赖问题
private OrderService self;
public void createOrder(Order order) {
System.out.println("准备创建订单...");
// 使用代理对象调用,事务生效!
self.insertOrder(order);
// 或者 ((OrderService)AopContext.currentProxy()).insertOrder(order);
// 后者需要额外配置 @EnableAspectJAutoProxy(exposeProxy = true)
}
public void insertOrder(Order order) {
orderMapper.insert(order);
if (order.getAmount() > 1000) {
throw new RuntimeException("订单金额过大,模拟异常!");
}
}
} -
【更推荐】重构代码,拆分到不同的Service:
这是更符合单一职责原则的做法。将需要事务控制的方法拆分到另一个Service中,通过注入该Service来调用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// OrderCreatorService.java
public class OrderCreatorService {
private OrderMapper orderMapper;
public void insertOrder(Order order) {
orderMapper.insert(order);
if (order.getAmount() > 1000) {
throw new RuntimeException("订单金额过大,模拟异常!");
}
}
}
// OrderServiceImpl.java
public class OrderServiceImpl implements OrderService {
private OrderCreatorService orderCreatorService;
public void createOrder(Order order) {
System.out.println("准备创建订单...");
// 通过注入的Bean调用,事务生效!
orderCreatorService.insertOrder(order);
}
}
元凶二:方法访问权限不是public
场景描述: 将 @Transactional 注解加在了 private、protected 或 default 权限的方法上。
失效原因: Spring AOP的默认实现(无论是基于JDK动态代理还是CGLIB)都要求被代理的方法是 public 的。对于非 public 方法,AOP无法进行拦截,也就无法织入事务逻辑。
错误示例:
1 |
|
解决方案: 非常简单,将方法的访问权限修改为 public。
元凶三:数据库引擎不支持事务
场景描述: 项目底层使用的数据库存储引擎本身就不支持事务。
失效原因: Spring的事务管理是建立在数据库本身支持事务的基础之上的。如果数据库引擎都不支持,Spring再怎么努力也是“巧妇难为无米之炊”。最典型的例子就是MySQL的 MyISAM 引擎。
检查与解决方案:
- 检查你的数据库表所使用的引擎。在MySQL中,可以使用
SHOW TABLE STATUS LIKE 'your_table_name';查看。 - 确保使用支持事务的引擎,如 InnoDB。
- 如果使用的是旧表,可以通过
ALTER TABLE your_table_name ENGINE=InnoDB;来修改。
元凶四:Bean没有被Spring容器管理
场景描述: 在一个没有被 @Component、@Service 等注解标记的类中使用了 @Transactional。
失效原因: @Transactional 的生效前提是,这个类的实例(Bean)必须是由Spring容器创建和管理的。只有这样,Spring才能为它创建代理对象。如果你自己 new 了一个对象,Spring对它一无所知,自然无法提供事务管理能力。
错误示例:
1 | public class ManualService { // 注意:没有 @Service 或 @Component 注解 |
解决方案:
将这个类交给Spring管理,添加 @Service、@Component 等注解,并通过 @Autowired 注入使用。
元凶五:异常被“吃掉”或异常类型不匹配
场景描述:
- 在事务方法内部使用了
try-catch块,捕获了异常但没有重新抛出。 - 抛出的异常类型不被Spring默认的回滚策略覆盖。
失效原因:
Spring判断事务是否回滚的默认依据是:方法在执行过程中,是否抛出了 RuntimeException 或 Error。
- 如果你用
try-catch捕获了异常并没有再往外抛,那么在Spring看来,这个方法是“正常执行”完成的,它自然会选择提交事务。 - 如果你抛出的是一个受检异常(Checked Exception,如
IOException或自定义的Exception),Spring默认不会回滚事务。
错误示例1:异常被“吃掉”
1 |
|
错误示例2:异常类型不匹配
1 |
|
解决方案:
-
对于被“吃掉”的异常: 在
catch块中处理完必要逻辑后,将异常重新抛出。throw new RuntimeException(e); -
对于异常类型不匹配:
- 方法一(推荐): 在业务代码中尽量使用或封装为
RuntimeException。 - 方法二(明确指定): 在
@Transactional注解中通过rollbackFor属性,明确指定需要回滚的异常类型。
1
2
3
4
5
6// 明确告诉Spring,遇到任何Exception都给我回滚!
public void updateStock(Long productId) throws MyCustomException {
// ...
throw new MyCustomException("自定义的业务异常!");
} - 方法一(推荐): 在业务代码中尽量使用或封装为
总结
让我们再次回顾这五大“元凶”:
- 方法自调用:
this调用绕过了代理。 - 非
public方法:AOP无法拦截。 - 数据库不支持:底层基础不具备。
- 非Spring Bean:对象未被Spring管理。
- 异常处理不当:异常被“吃掉”或类型不匹配。
掌握了这些,相信你对Spring事务的理解又上了一个台阶。记住,技术用得爽,原理不能忘。理解了AOP代理这个核心,很多问题都会迎刃而解。
希望这篇博客能帮你扫清知识盲区,在未来的开发中,让 @Transactional 成为你手中真正稳定可靠的神器!

