Spring 事务失效可能是哪些原因?
面试考察点
- AOP 原理理解:面试官不仅仅是想让你罗列几个场景,更是想知道你是否真正理解 Spring 事务基于 AOP 动态代理的底层机制。大部分事务失效场景的根因都在这里。
- 实战踩坑经验:考察你在真实项目中是否遇到过事务失效的问题,以及排查思路。只会背八股和真正踩过坑的人,回答的深度完全不一样。
- 编码规范意识:看你是否了解 Spring 事务使用的最佳实践,能不能在编码阶段就规避这些坑。
核心答案
Spring 事务失效的常见原因,我按 "出现频率" 从高到低梳理了 7 大类:
| 序号 | 失效场景 | 根本原因 | 严重程度 |
|---|---|---|---|
| 1 | 同一个类内部方法调用 | 未经过代理对象,AOP 不生效 | ⭐⭐⭐⭐⭐ |
| 2 | 方法不是 public | Spring AOP 默认只代理 public 方法 | ⭐⭐⭐⭐⭐ |
| 3 | 方法被 final 或 static 修饰 | 无法被代理类重写 | ⭐⭐⭐⭐ |
| 4 | 异常被 try-catch 吞掉 | 代理对象感知不到异常,不会触发回滚 | ⭐⭐⭐⭐⭐ |
| 5 | 抛出的异常类型不对 | 默认只回滚 RuntimeException,不回滚受检异常 | ⭐⭐⭐⭐ |
| 6 | 数据库引擎不支持事务 | 比如 MySQL 的 MyISAM 引擎 | ⭐⭐⭐ |
| 7 | 事务传播行为配置错误 | 比如 NOT_SUPPORTED、NEVER 本身就不走事务 | ⭐⭐⭐ |
深度解析
一、同类内部方法调用(最常见!)
这是事务失效的 "头号杀手"。看下面这段代码:
@Service
public class UserService {
public void createUser(User user) {
// 内部直接调用,事务不生效!
this.saveUser(user);
}
@Transactional
public void saveUser(User user) {
userMapper.insert(user);
// 模拟异常
int i = 1 / 0;
}
}
createUser() 调用 saveUser() 时,用的是 this.saveUser(),也就是目标对象本身的方法调用,根本没走代理对象。Spring 事务是基于 AOP 动态代理的,绕过了代理,事务注解就是个摆设。
上图展示了 Spring 事务代理的调用链路。核心区别在于:
- 外部调用(生效):
Controller→ 代理对象(在方法前后开启/提交事务) → 目标对象。经过代理对象,事务拦截器正常工作。 - 内部调用(失效):目标对象通过
this.直接调用自身方法,完全绕过了代理对象,事务拦截器根本没机会执行。
解决方案:有三种方式——
// 方式 1:注入自身(推荐,最简洁)
@Service
public class UserService {
@Autowired
private UserService self;
public void createUser(User user) {
self.saveUser(user); // 通过代理对象调用
}
}
// 方式 2:从 ApplicationContext 获取代理对象
@Service
public class UserService {
@Autowired
private ApplicationContext context;
public void createUser(User user) {
context.getBean(UserService.class).saveUser(user);
}
}
// 方式 3:使用 AopContext(需要在启动类加 @EnableAspectJAutoProxy(exposeProxy = true))
public void createUser(User user) {
((UserService) AopContext.currentProxy()).saveUser(user);
}
二、方法不是 public
Spring AOP 创建代理时,会检查目标方法的访问修饰符。private、protected、包级可见的方法,事务注解直接被忽略。
@Service
public class OrderService {
// ❌ 事务不生效!
@Transactional
private void createOrder() { ... }
// ❌ 事务不生效!
@Transactional
protected void updateOrder() { ... }
// ✅ 事务生效
@Transactional
public void deleteOrder() { ... }
}
源码层面,SpringTransactionAnnotationParser 解析事务注解时,AbstractFallbackTransactionAttributeSource 会在 computeTransactionAttribute() 方法中做访问修饰符的校验,非 public 方法直接返回 null,不创建事务属性。
三、方法被 final 或 static 修饰
final 方法无法被子类重写,static 方法属于类而非实例。Spring CGLIB 代理通过生成子类并重写方法来实现代理,这两种修饰符都会导致代理失败。
@Service
public class UserService {
// ❌ final 方法无法被 CGLIB 重写,事务不生效
@Transactional
public final void saveUser(User user) { ... }
// ❌ static 方法不参与代理
@Transactional
public static void batchInsert(List<User> users) { ... }
}
四、异常被 try-catch 吞掉
这个坑我当年确实踩过,排查了半天才发现异常被吞了。
@Service
public class PayService {
@Transactional
public void pay(Order order) {
try {
orderMapper.update(order);
accountMapper.deduct(order.getAmount());
int i = 1 / 0; // 抛出异常
} catch (Exception e) {
// ❌ 异常被吞掉,代理对象感知不到异常
// 事务管理器认为方法正常返回,直接提交事务
log.error("支付失败", e);
}
}
}
Spring 的事务管理器靠捕获方法抛出的异常来决定是否回滚。异常被 catch 住了,事务管理器以为一切正常,直接 commit。
解决方案:
// 方式 1:catch 后手动抛出
catch (Exception e) {
log.error("支付失败", e);
throw new RuntimeException(e); // 重新抛出
}
// 方式 2:catch 后手动标记回滚
catch (Exception e) {
log.error("支付失败", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
// 方式 3:真正需要处理异常的话,别在事务方法里 catch
五、抛出的异常类型不对
Spring 事务默认 只对 RuntimeException 和 Error 回滚,不对受检异常(checked exception)回滚。这是很多初学者不知道的。
@Service
public class UserService {
// ❌ 默认只回滚 RuntimeException,IOException 是受检异常,不会回滚
@Transactional
public void importUsers(File file) throws IOException {
userMapper.batchInsert(parse(file));
// 抛出受检异常,事务不会回滚!
throw new IOException("文件读取失败");
}
// ✅ 指定回滚所有异常
@Transactional(rollbackFor = Exception.class)
public void importUsers2(File file) throws IOException {
userMapper.batchInsert(parse(file));
throw new IOException("文件读取失败");
}
}
最佳实践:所有 @Transactional 都加上 rollbackFor = Exception.class,养成习惯。别嫌麻烦,省得以后排查半天。
六、数据库引擎不支持事务
这个比较冷门,但确实有人踩过。MySQL 中 MyISAM 引擎不支持事务,只有 InnoDB 支持。如果表的引擎是 MyISAM,@Transactional 写上天也没用。
-- 查看表引擎
SHOW TABLE STATUS LIKE 't_user';
-- 修改表引擎为 InnoDB
ALTER TABLE t_user ENGINE = InnoDB;
七、事务传播行为配置错误
@Transactional 的 propagation 参数如果不小心配错了,事务也会 "失效":
// 以非事务方式执行,如果当前存在事务则挂起
@Transactional(propagation = Propagation.NOT_SUPPORTED)
// 以非事务方式执行,如果当前存在事务则抛出异常
@Transactional(propagation = Propagation.NEVER)
// 注意:MANDATORY 是反过来的——要求必须存在事务,否则抛异常
// 这个不是事务失效,而是必须要有外层事务
@Transactional(propagation = Propagation.MANDATORY)
还有几种不太常见但也会遇到的情况:
- 未被 Spring 容器管理:类没加
@Service、@Component等注解,或者是用new创建的对象,根本不在 Spring 容器中,代理也就无从谈起了。 - 多线程场景:Spring 事务是基于
ThreadLocal的,事务信息绑定到当前线程。如果方法里开了新线程,新线程里的事务操作和原线程的事务是独立的,不会回滚。 - 嵌套事务回滚异常:
Propagation.REQUIRES_NEW的内层事务回滚时,如果抛出异常被外层 catch,外层事务可能会因为UnexpectedRollbackException而失败。
面试高频追问
-
追问一:Spring 事务的底层实现原理是什么?
- 基于 AOP 动态代理。通过
TransactionInterceptor拦截@Transactional方法,在方法执行前开启事务,执行后根据异常情况决定提交或回滚。核心类是AbstractPlatformTransactionManager。
- 基于 AOP 动态代理。通过
-
追问二:
@Transactional的rollbackFor属性默认值是什么?- 默认只回滚
RuntimeException和Error。如果需要回滚受检异常,必须显式指定rollbackFor = Exception.class。
- 默认只回滚
-
追问三:Spring 事务的传播行为有哪些?常用的有哪几个?
- 7 种传播行为。最常用的是
REQUIRED(默认,有事务就加入,没有就新建)和REQUIRES_NEW(总是新建事务,挂起当前事务)。
- 7 种传播行为。最常用的是
-
追问四:如何在同一个类中让内部方法调用也走事务?
- 注入自身代理对象(
@Autowired自身)、从ApplicationContext获取代理对象、使用AopContext.currentProxy()。
- 注入自身代理对象(
常见面试变体
- "说说
@Transactional注解有哪些坑?" - "Spring 事务在什么情况下不会回滚?"
- "为什么 Spring 事务失效?你是怎么排查的?"
- "
@Transactional的rollbackFor属性有什么用?"
记忆口诀
私(非 public)静(static)终(final)内(同类调用)吞(异常被 catch)错(异常类型不对)引(引擎不支持)
翻译一下:非 public、static、final、同类内部调用、异常被吞、异常类型错、引擎不支持事务。前四个都是 "代理不生效",后三个是 "事务不回滚"。
总结
Spring 事务失效的根因就两个方向:代理没生效(同类调用、非 public、final/static、未被 Spring 管理)和事务没回滚(异常被吞、异常类型不对、引擎不支持、传播行为配错)。排查的时候沿着这条线逐一排查,基本不会漏。
