📌 PDF :大白话说Java面试题 --- 06_Spring篇
第20题:Spring 事务什么时候会失效?
📚 回答:
- 核心考点 : Spring 事务失效 是面试中最高频的考点之一,大厂面试官不会满足于背诵"非 public、同类调用"等表面原因,而是期望你深入理解 AOP 代理机制的本质 (JDK 动态代理 vs CGLIB 的字节码重写边界)、事务拦截的完整链路 (从
TransactionInterceptor到ThreadLocal绑定)、异常回滚的精确语义 (默认仅回滚 RuntimeException/Error),以及 传播行为对事务边界的影响。面试官真正想判断的是:你是否能从源码级理解"为什么失效",以及能否在生产环境中快速定位和修复事务问题。
1. 代理机制导致的失效(4 种场景)
Spring 声明式事务基于 AOP 代理实现,任何导致代理无法生效的情况都会使事务失效。
-
1.1 方法非 public
原因 :
TransactionInterceptor通过 AOP 代理拦截方法调用,而 JDK 动态代理只能代理接口的 public 方法,CGLIB 代理虽然可以代理类,但 Spring 的事务属性解析器AbstractFallbackTransactionAttributeSource明确过滤了非 public 方法:java// AbstractFallbackTransactionAttributeSource 源码 protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) { // 只允许 public 方法 if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; // 非 public 方法直接返回 null,不生成事务属性 } // ... }示例:
java@Service public class UserService { @Transactional private void addUser() { // ❌ 事务失效! userDao.insert(user); } }解决方案 :将方法改为
public。citation:0 -
1.2 方法被 final/static 修饰
原因:
- final 方法 :CGLIB 代理通过生成子类并重写父类方法实现代理,
final方法无法被重写,代理对象无法插入事务逻辑。 - static 方法:静态方法属于类而非实例,CGLIB 代理的是实例对象,无法拦截静态方法调用。
示例:
java@Service public class UserService { @Transactional public final void addUser() { // ❌ final 方法,事务失效! userDao.insert(user); } @Transactional public static void deleteUser() { // ❌ static 方法,事务失效! userDao.delete(id); } }解决方案 :移除
final/static修饰符。citation:1 - final 方法 :CGLIB 代理通过生成子类并重写父类方法实现代理,
-
1.3 Bean 未被 Spring 容器管理
原因 :Spring 事务依赖 IoC 容器创建代理对象。通过
new关键字创建的对象是普通 Java 对象,没有经过BeanPostProcessor处理,不会生成事务代理。示例:
java// ❌ 错误:new 出来的对象没有代理 UserService userService = new UserService(); userService.addUser(); // 事务失效! // ✅ 正确:从 Spring 容器获取代理对象 @Autowired private UserService userService; userService.addUser(); // 事务生效 -
1.4 同类方法自调用(this 调用)
原因 :
this.methodB()调用的是目标对象本身的方法,而非代理对象的方法。事务逻辑(开启、提交、回滚)编织在代理对象中,直接调用目标对象绕过了TransactionInterceptor。示例:
java@Service public class OrderService { // ❌ 错误:methodA 调用 methodB 不走代理 public void methodA() { this.methodB(); // this 指向目标对象,非代理对象 } @Transactional public void methodB() { orderDao.insert(order); } }三种解决方案:
方案 代码示例 适用场景 注入自身代理 @Autowired private OrderService self; self.methodB();最常用,简单直接 AopContext ((OrderService) AopContext.currentProxy()).methodB();需配置 @EnableAspectJAutoProxy(exposeProxy = true)拆分到新 Bean 将 methodB移到OrderService2中注入调用彻底解耦,但增加类数量 java// ✅ 方案一:注入自身代理(推荐) @Service public class OrderService { @Autowired private OrderService self; // 注入的是代理对象 public void methodA() { self.methodB(); // 通过代理调用,事务生效 } @Transactional public void methodB() { orderDao.insert(order); } }
2. 异常处理导致的失效(3 种场景)
-
2.1 异常被 try-catch 吞掉
原因 :
TransactionInterceptor通过捕获方法抛出的异常来决定回滚。如果异常在方法内部被 catch 且未重新抛出,拦截器感知不到异常,会正常提交事务。示例:
java@Transactional public void addUser() { try { userDao.insert(user); int i = 1 / 0; // 抛出 ArithmeticException } catch (Exception e) { log.error("异常:", e); // ❌ 异常被吞掉,事务正常提交! } }解决方案:
java@Transactional public void addUser() { try { userDao.insert(user); int i = 1 / 0; } catch (Exception e) { log.error("异常:", e); // ✅ 手动标记回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // 或重新抛出:throw new RuntimeException(e); } } -
2.2 抛出 checked Exception(默认不回滚)
原因 :
TransactionInterceptor的默认回滚规则只针对RuntimeException和Error。checked Exception(如IOException、SQLException)默认不会触发回滚。java// TransactionInterceptor 源码中的回滚判断 protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) { if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) { // 回滚 } else { // 提交! } } // DefaultTransactionAttribute 的默认实现 public boolean rollbackOn(Throwable ex) { return (ex instanceof RuntimeException || ex instanceof Error); }示例:
java@Transactional // ❌ 默认配置,IOException 不会回滚! public void addUser() throws IOException { userDao.insert(user); throw new IOException("IO 异常"); // checked 异常,事务提交! }解决方案:
java@Transactional(rollbackFor = Exception.class) // ✅ 显式配置回滚所有异常 public void addUser() throws IOException { userDao.insert(user); throw new IOException("IO 异常"); // 现在会回滚 } -
2.3 错误配置 noRollbackFor
原因 :
noRollbackFor显式排除了某些异常类型的回滚,如果配置不当,本应回滚的异常被排除。java@Transactional(noRollbackFor = RuntimeException.class) // ❌ 危险配置! public void addUser() { userDao.insert(user); throw new RuntimeException("异常"); // 不会回滚! }
3. 配置与基础设施导致的失效(4 种场景)
-
3.1 未启用事务管理
@EnableTransactionManagement未添加或事务管理器未配置:java// ❌ 错误:缺少 @EnableTransactionManagement @Configuration public class AppConfig { @Bean public DataSourceTransactionManager transactionManager(DataSource ds) { return new DataSourceTransactionManager(ds); } } // ✅ 正确 @Configuration @EnableTransactionManagement // 必须添加! public class AppConfig { @Bean public PlatformTransactionManager transactionManager(DataSource ds) { return new DataSourceTransactionManager(ds); } } -
3.2 多数据源未指定事务管理器
原因 :当存在多个
PlatformTransactionManagerBean 时,Spring 不知道使用哪个,可能选择错误的(或默认的)事务管理器。java// ❌ 错误:有两个数据源,但未指定用哪个事务管理器 @Transactional // 可能使用了错误的事务管理器 public void addUser() { ... } // ✅ 正确:显式指定 @Transactional(transactionManager = "orderTransactionManager") public void addUser() { ... } -
3.3 数据库引擎不支持事务
原因:MySQL 的 MyISAM 引擎不支持事务,即使 Spring 事务配置正确,数据库层面也不会回滚。
sql-- ❌ MyISAM 不支持事务 CREATE TABLE user (...) ENGINE=MyISAM; -- ✅ InnoDB 支持事务 CREATE TABLE user (...) ENGINE=InnoDB; -
3.4 异步方法(@Async)导致事务上下文丢失
原因 :
@Async会在新线程执行方法,Spring 事务通过ThreadLocal绑定连接,新线程没有父线程的事务上下文。java@Transactional public void parentMethod() { userDao.insert(user); asyncService.asyncMethod(); // ❌ 新线程,事务上下文丢失! } @Async public void asyncMethod() { orderDao.insert(order); // 不在 parentMethod 的事务中 }解决方案 :在异步方法内部单独加
@Transactional,或使用分布式事务框架。
4. 传播行为导致的"伪失效"(2 种场景)
-
4.1 NOT_SUPPORTED 挂起事务
java@Transactional public void methodA() { // 在事务 T1 中 serviceB.methodB(); // T1 被挂起,methodB 非事务执行 } @Transactional(propagation = Propagation.NOT_SUPPORTED) public void methodB() { orderDao.insert(order); // 非事务执行,不会回滚 }这不是真正的"失效",而是传播行为的预期效果,但开发者容易误以为是事务失效。
-
4.2 NEVER 强制无事务
java@Transactional public void methodA() { serviceB.methodB(); // ❌ 抛出 IllegalTransactionStateException } @Transactional(propagation = Propagation.NEVER) public void methodB() { orderDao.insert(order); }
5. 全场景失效速查表
| 失效场景 | 根本原因 | 检测方法 | 解决方案 |
|---|---|---|---|
| 非 public 方法 | AbstractFallbackTransactionAttributeSource 过滤非 public |
检查方法修饰符 | 改为 public |
| final 方法 | CGLIB 无法重写 final 方法 | 检查 final 修饰符 |
移除 final |
| static 方法 | 静态方法不属于实例,代理无法拦截 | 检查 static 修饰符 |
移除 static |
| 未纳入 Spring 容器 | new 创建的对象无代理 |
检查对象来源 | 通过 @Autowired 注入 |
| 同类 this 自调用 | 直接调用目标对象,绕过代理 | 检查调用方式 | 注入自身代理 / AopContext / 拆分到新 Bean |
| 异常被 try-catch 吞掉 | 拦截器感知不到异常 | 检查异常处理逻辑 | catch 后重新抛出或手动 setRollbackOnly() |
| checked 异常默认不回滚 | rollbackOn() 只判断 RuntimeException/Error |
检查异常类型和 rollbackFor 配置 |
配置 rollbackFor = Exception.class |
| noRollbackFor 配置错误 | 显式排除了应回滚的异常 | 检查 noRollbackFor 配置 |
修正配置 |
| 未加 @EnableTransactionManagement | AOP 事务代理未启用 | 检查启动类/配置类 | 添加注解 |
| 多数据源未指定管理器 | Spring 选择错误的事务管理器 | 检查 transactionManager 配置 |
显式指定 transactionManager |
| 数据库引擎不支持 | MyISAM 等引擎无事务能力 | 检查表引擎 | 改用 InnoDB |
| @Async 新线程 | ThreadLocal 事务上下文丢失 | 检查异步调用 | 异步方法内单独加事务或改用分布式事务 |
| NOT_SUPPORTED/NEVER | 传播行为预期效果 | 检查传播行为配置 | 根据业务语义调整 |
6. 生产环境排查事务问题的 4 步法
| 步骤 | 操作 | 工具/命令 |
|---|---|---|
| Step 1:确认代理是否生效 | 检查对象是否为代理对象 | AopUtils.isAopProxy(userService) |
| Step 2:确认事务属性是否解析 | 检查方法是否有 TransactionAttribute | TransactionAspectSupport.currentTransactionStatus() |
| Step 3:确认异常是否传播到拦截器 | 检查异常是否被 catch 吞掉 | 断点调试 TransactionInterceptor.invoke() |
| Step 4:确认数据库层面是否回滚 | 检查数据库引擎和连接 autoCommit | SHOW CREATE TABLE user; / 查看连接属性 |
7. 面试官追问与高分回答模板
-
追问 1:"Spring 事务什么时候会失效?请尽可能多说几种。"
低分回答:"非 public 方法、同类方法自调用、异常被 catch。"(只说了最常见的3种,不够全面)
高分回答 : > "Spring 事务失效可以从 代理机制、异常处理、配置基础设施、传播行为 四个维度分类: > 1. 代理机制失效 :方法非 public(
AbstractFallbackTransactionAttributeSource过滤)、final/static 方法(CGLIB 无法重写)、Bean 未被 Spring 容器管理(new创建)、同类 this 自调用(绕过代理)。 > 2. 异常处理失效 :异常被 try-catch 吞掉(拦截器感知不到)、抛出 checked Exception(默认不回滚,需配置rollbackFor)、错误配置noRollbackFor。 > 3. 配置基础设施失效 :未加@EnableTransactionManagement、多数据源未指定事务管理器、数据库引擎不支持(如 MyISAM)、@Async新线程丢失 ThreadLocal 上下文。 > 4. 传播行为"伪失效" :NOT_SUPPORTED挂起事务、NEVER强制无事务。 > 生产环境中,最常见的是 同类自调用 和 异常被吞掉 这两个坑。" -
追问 2:"同类方法自调用为什么事务会失效?怎么解决?"
高分回答 : > "根本原因是 Spring AOP 基于代理实现 。
this.methodB()中的this指向的是目标对象(原始 Bean 实例),而非 Spring 容器创建的代理对象。事务逻辑(TransactionInterceptor的开启、提交、回滚)都编织在代理对象中,直接调用目标对象完全绕过了拦截器。 > 解决方案有三种: > 1. 注入自身代理对象 :@Autowired private OrderService self;然后self.methodB(),最常用; > 2. 使用AopContext.currentProxy():获取当前代理对象后调用,需配置@EnableAspectJAutoProxy(exposeProxy = true); > 3. 拆分到另一个 Bean :将methodB提取到OrderService2中,通过@Autowired注入调用,彻底解耦。" -
追问 3:"为什么 checked 异常默认不回滚?什么情况下需要配置 rollbackFor?"
高分回答 : > "Spring 的默认回滚规则在
DefaultTransactionAttribute.rollbackOn()中定义:只回滚RuntimeException和Error。这是设计选择------RuntimeException代表编程错误或不可恢复异常,应该回滚;而 checked Exception(如IOException)代表业务异常,业务层可能已经做了补偿处理,强制回滚可能破坏业务逻辑。 > 但在实际开发中,很多业务异常继承自Exception(checked),如果希望这些异常也触发回滚,必须显式配置@Transactional(rollbackFor = Exception.class)。 > 另一个常见坑是:即使配置了rollbackFor,如果异常在方法内被 try-catch 吞掉未抛出,拦截器依然感知不到,事务会正常提交。" -
追问 4:"如果事务方法里调用了 @Async 异步方法,异步方法里的操作会参与事务吗?"
高分回答 : > "不会参与 。Spring 事务通过
TransactionSynchronizationManager中的ThreadLocal绑定数据库连接,@Async会在新线程中执行方法,新线程没有父线程的 ThreadLocal 上下文,因此异步方法完全独立于父事务。 > 如果异步操作需要事务,必须在异步方法内部单独加@Transactional。如果要求异步操作随父事务回滚,必须使用分布式事务框架(如 Seata),或者改为同步执行。" -
追问 5:"怎么快速排查生产环境中的事务失效问题?"
高分回答 : > "我通常按四步法排查: > 1. 确认代理是否生效 :打印
AopUtils.isAopProxy(service),如果返回 false,说明 Bean 未被代理(可能是new创建或配置错误)。 > 2. 确认事务属性是否解析 :在方法内调用TransactionAspectSupport.currentTransactionStatus(),如果抛出异常说明事务未开启。 > 3. 确认异常是否传播到拦截器 :在TransactionInterceptor.invoke()打断点,看异常是否到达completeTransactionAfterThrowing()方法。 > 4. 确认数据库层面是否回滚 :检查表引擎(SHOW CREATE TABLE)和连接的autoCommit状态。 > 另外,开启 Spring 事务 DEBUG 日志(logging.level.org.springframework.transaction=DEBUG)可以看到完整的事务开启、提交、回滚日志。" -
追问 6:"final 方法为什么事务会失效?static 方法呢?"
高分回答 : > "两者原因不同: > - final 方法 :Spring 默认使用 CGLIB 代理(当目标类没有实现接口时),CGLIB 通过生成目标类的子类并重写父类方法来插入事务逻辑。
final方法无法被重写,因此代理对象无法拦截,事务失效。 > - static 方法 :静态方法属于类级别,不绑定到实例对象。CGLIB 代理的是实例对象,通过MethodInterceptor.intercept()拦截实例方法调用,无法拦截静态方法。即使使用 JDK 动态代理(基于接口),接口中也不允许定义 static 方法(Java 8 之前)。 > 解决方案都是移除final/static修饰符,或将逻辑移到实例方法中。"
8. 方案选型速查表
| 问题类型 | 推荐排查/解决方案 | 优先级 |
|---|---|---|
| 事务完全没生效 | 检查 @EnableTransactionManagement、方法是否为 public、Bean 是否被容器管理 |
P0 |
| 异常未触发回滚 | 检查异常是否被 catch、是否为 checked 异常、rollbackFor 配置 |
P0 |
| 部分方法无事务 | 检查同类自调用、final/static 修饰、传播行为配置 | P1 |
| 异步方法事务异常 | 检查 @Async 与 @Transactional 的线程上下文 |
P1 |
| 偶发性数据不一致 | 检查数据库引擎(MyISAM vs InnoDB)、多数据源事务管理器 | P1 |
💡 面试官想要的满分总结:
Spring 事务失效的本质是 AOP 代理机制被打破 或 事务拦截链路被绕过。理解失效场景必须从源码级抓住三个核心:
- 代理是前提 :只有经过代理对象调用的 public 实例方法才会被
TransactionInterceptor拦截。同类自调用(this)、final/static 方法、非容器管理的 Bean 都绕过了代理。- 异常是信号 :
TransactionInterceptor通过捕获异常触发回滚。异常被 try-catch 吞掉、checked 异常默认不回滚、错误配置noRollbackFor都会导致"该回滚的没回滚"。- 上下文是纽带 :事务通过
ThreadLocal绑定连接,@Async新线程会丢失上下文;多数据源需要显式指定事务管理器;数据库引擎(MyISAM)不支持事务则 Spring 事务完全无效。生产环境排查事务问题,记住四步法:确认代理生效 → 确认事务属性解析 → 确认异常传播到拦截器 → 确认数据库层面支持。同时开启 DEBUG 日志是最有效的定位手段。最后,同类自调用和异常被吞掉是实际开发中最隐蔽、最高频的两个坑,必须形成肌肉记忆。
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯