真实面经题目 · 原创解析

Spring 事务失效有哪些常见场景?

Spring 事务失效不要只背 @Transactional 场景清单,核心要答出声明式事务的运行条件:方法调用必须进入 Spring AOP 代理,由 TransactionInterceptor 配合 TransactionManager 在调用前后开启、提交或回滚事务;常见失效本质上分为三类:没有经过代理、异常没有触发回滚、真实数据库资源没有加入同一个事务。

出现于:阿里巴巴 · 后端开发

60 秒回答模板

Spring 事务失效首先看调用链有没有进入代理,因为 @Transactional 不是编译期魔法,而是 Spring 用 AOP 代理包了一层事务拦截器;同类自调用、private/final 方法、new 出来的对象、绕过 Spring Bean 调用都会导致拦截器不执行。第二看异常回滚规则,默认只对 RuntimeException 和 Error 回滚,受检异常要配置 rollbackFor,异常被 catch 吞掉也不会回滚。第三看传播行为和边界,REQUIRED 会加入外层物理事务,内层标记 rollback-only 可能导致外层提交时抛 UnexpectedRollbackException,REQUIRES_NEW 又会开启独立事务并占用额外连接。第四看执行环境,异步线程、手动开线程、消息回调、外部 IO 都不天然继承当前事务。最后还要看底层资源,比如 MySQL 表是否支持事务、DDL 是否隐式提交、多数据源是否用了正确的事务管理器、readOnly 是否被误解为强制禁止写。这样答比罗列场景更能体现你理解的是代理、边界、异常、资源四件事。

考点 代理机制
主线 自调用问题
易错点 只回答“自调用、异常、private”几个词,没有解释…

深入解析

01

代理机制

Spring 声明式事务的第一层原理是 AOP 代理。@Transactional 只是事务元数据,真正执行的是代理对象上的 TransactionInterceptor:进入方法前根据传播行为拿事务,方法正常返回则提交,抛出符合规则的异常则标记回滚。所以判断是否失效,先问这个调用是不是从 Spring 容器拿到的代理对象发起的,而不是直接调用目标对象。

02

自调用问题

最典型场景是同一个类里 methodA 调 methodB,而 methodB 上加了 @Transactional。这个调用本质是 this.methodB,不会经过代理对象,因此事务拦截器没有机会执行。解决思路不是简单把注解再贴几遍,而是把事务方法拆到另一个 Service,通过 Spring 注入调用;或者注入自身代理;复杂场景才考虑 AspectJ weaving。面试中要强调,同类内部调用和自调用是同一个根因:绕过代理。

03

方法可见性

方法修饰符也会影响事务。JDK 动态代理代理的是接口方法,因此事务方法通常要是接口上可见的 public 方法;CGLIB 是子类代理,但 final 类、final 方法、private 方法无法被子类覆盖,也就无法被增强。即使较新的 Spring 版本对非 public 方法支持更好,工程实践里仍建议把事务边界放在清晰的 public Service 方法上,减少代理类型差异带来的误判。

04

Bean 入口

事务方法必须运行在 Spring 管理的 Bean 上。常见失效包括自己 new Service、在构造方法里调用事务方法、通过静态工具类调用、把对象交给非 Spring 框架直接实例化、或者在初始化阶段提前调用还没代理完成的对象。此时即使方法上有 @Transactional,也只是普通注解。事务边界应放在由容器注入并被外部调用的业务 Service 入口,而不是任意类的任意方法。

05

回滚规则

异常规则是第二大类失效。默认情况下 Spring 只对 RuntimeException 和 Error 回滚,普通 checked exception 不会触发回滚;如果业务方法抛 IOException、SQLException 这类受检异常,需要显式配置 rollbackFor。另一个高频坑是 catch 异常后只记录日志不再抛出,代理看到的是正常返回,自然会提交。需要吞异常但回滚时,应重新抛出异常或在事务上下文中显式设置 rollback-only。

06

rollbackFor

rollbackFor 不是装饰性配置,而是用来补足业务异常体系和 Spring 默认规则之间的差距。很多项目会定义 BusinessException,如果它继承 Exception 而不是 RuntimeException,默认不会回滚;如果配置 rollbackFor = Exception.class,又要警惕把本来不该回滚的流程性异常也纳入回滚。更稳的做法是统一业务异常基类和回滚语义,避免异常规则过宽或过窄。

07

传播行为

传播行为不是失效原因本身,但配置错会表现为事务不符合预期。REQUIRED 是默认值,内层会加入外层同一个物理事务;内层即使被 catch,只要已经把事务标成 rollback-only,外层最后提交时也可能抛 UnexpectedRollbackException。REQUIRES_NEW 会挂起外层并开启独立物理事务,适合审计日志等必须单独提交的场景,但会多占数据库连接。NESTED 依赖保存点,不同事务管理器和数据库支持不同。

08

事务边界

事务边界设计不合理也会被误认为失效。边界太小,比如只给 DAO 某个写方法加事务,而上层有多步业务校验和多表写入,失败时无法保证整体一致;边界太大,比如把远程调用、文件上传、消息发送、慢查询都包进事务,会导致锁持有时间过长。更推荐以业务用例为单位放在 Service 层,让一个用例内需要原子性的数据库操作共享同一个事务,外部副作用通过事务后事件或可靠消息处理。

09

异步线程

事务和线程绑定是必须说清楚的点。普通 PlatformTransactionManager 会把连接、同步状态等绑定到当前线程,新开 Thread、线程池任务、@Async 方法、CompletableFuture 后续阶段都不会天然继承原事务。于是主线程回滚不代表异步任务也回滚,异步任务里如果要事务,需要在异步方法自己的代理入口上声明事务。涉及消息、通知、缓存刷新时,更稳的是使用事务提交后的回调或 outbox 模式。

10

底层资源

最后要落到底层数据库和资源。MySQL 里 InnoDB 支持事务、回滚和崩溃恢复,而 MyISAM 等非事务表不会被 ROLLBACK 撤销;DDL、TRUNCATE 等语句可能隐式提交或不可回滚;多数据源场景如果 @Transactional 绑定了错误的 transactionManager,实际写入的 DataSource 就不会加入该事务;手动用 DataSource.getConnection 绕过 Spring 的连接获取,也可能拿不到线程绑定连接。

易错点

  • 只回答“自调用、异常、private”几个词,没有解释 Spring 事务是代理拦截器机制。
  • 认为只要方法上加了 @Transactional 就一定生效,忽略对象必须是 Spring 容器里的代理对象。
  • 把事务注解加在 private、final 或内部直接调用的方法上,还以为能被 AOP 拦截。
  • 以为所有异常都会回滚,忘记默认 checked exception 不回滚,也忘记异常被 catch 后会正常提交。
  • 滥用 rollbackFor = Exception.class,不区分业务异常、流程异常和真正需要回滚的数据异常。
  • 把 REQUIRES_NEW 当成普通嵌套事务,忽略它会开启独立物理事务并额外占用连接。
  • 认为 @Transactional(readOnly = true) 一定禁止写入,实际上它更多是只读提示和优化信号。

面试官追问

为什么同一个类里调用 @Transactional 方法会失效?怎么解决?

因为 Spring 默认是代理模式,只有外部调用经过代理对象时,事务拦截器才会执行。同类内部调用等价于 this.xxx,直接调用目标对象方法,没有进入代理。常见解决方式是把被调用的事务方法拆到另一个 Service,通过 Spring 注入调用;或者注入当前类的代理对象再调用;如果项目确实需要拦截内部调用,可以考虑 AspectJ weaving,但复杂度更高。

抛了异常为什么没有回滚?

要分两种情况看。第一,异常类型不符合默认规则,Spring 默认只回滚 RuntimeException 和 Error,checked exception 需要配置 rollbackFor。第二,异常被业务代码 catch 后没有继续抛出,代理层看到方法正常返回,就会提交事务。如果需要记录日志后仍回滚,应重新抛出异常,或者在事务上下文中显式标记 rollback-only。

REQUIRED 和 REQUIRES_NEW 有什么区别,为什么会影响事务结果?

REQUIRED 是默认传播行为,有外层事务就加入,没有才新建,所以多个方法可能共享同一个物理事务;内层一旦把事务标记为 rollback-only,外层最终提交也会失败。REQUIRES_NEW 会挂起外层事务,开启独立物理事务,内外提交回滚互不直接影响,适合审计日志等场景,但它需要额外连接,连接池太小会带来阻塞风险。

@Async 和 @Transactional 同时使用会怎样?

关键看入口经过哪个代理以及在哪个线程执行。主线程事务不会自动传播到 @Async 的线程池线程,因为命令式事务资源通常绑定在线程本地变量上。若异步逻辑也要事务,应把 @Transactional 放在异步方法自己的代理入口上,让异步线程内部重新开启事务。若想在主事务提交后再发送消息或通知,应使用事务提交后回调或可靠消息方案。

@Transactional(readOnly = true) 会不会强制禁止写?

不一定。Spring 的 readOnly 更准确地说是给事务子系统、JDBC 驱动或 ORM 的提示,可能带来连接只读、Hibernate 跳过脏检查等优化,但不是所有数据库和事务管理器都会强制拒绝写入。因此不能把 readOnly 当作权限控制。写方法放在 readOnly 事务里,可能表现为不 flush、抛异常、或依然写成功,取决于具体技术栈。

多数据源下为什么 @Transactional 看起来生效但数据没回滚?

因为事务管理器和真实执行 SQL 的数据源可能不是同一个。比如注解默认选了主库的 transactionManager,但方法里通过另一个 SqlSession、JdbcTemplate 或 EntityManager 写了从另一个 DataSource 获取的连接,这部分操作就不在当前事务里。多数据源要明确指定 transactionManager,并保证 ORM、连接池、路由数据源和事务边界使用同一套资源。