面试记录 2026-09-28
题目:Spring事务失效可能是哪些原因?
难度:difficulty/medium
标签:spring/transaction, general/knowledge, difficulty/medium
评分:⭐⭐⭐☆☆(3/5)
我的回答
Spring 事务失效。首先 Spring 事务是它自身不提供事务,它是依赖于所属数据库支持事务才能使用事务。另外,Spring 的事务它是基于自身代理实现的,通过事务注解标注,然后通过代理对象来实现事务的。在事务代理的实现过程中,它需要首先关闭自动提交,然后在启动方法前进行启动事务,启动方法后进行提交事务,这么一整个流程。这里就限定了。首先第一点,你这个对象这里边就隐含了一些它失败的原因。首先你这个对象是要在 Spring Bean 的管理管理范围内呢,它需要是个代理。另外,它需要执行代理的方法才能执行事务。如果你是通过直接 new 或者是在本方法内去调用这个方,事务方法,它是无法进行事务支撑的。
点评
优点:
- 数据库层支持事务是前提
- 抓到了 Spring 事务基于 AOP 代理的核心前提
- 事务代理流程讲对了:关闭自动提交 → begin → commit
- 两个直接失效场景抓到了:直接 new 和 类内部自调用
不足(严重):
- 说"这里就限定了"后只给了 3 条失效原因,清单严重不全
- 方法可见性失效漏了:private / final / static 方法无法被代理覆写
- 非 public 方法漏了:JDK 动态代理只支持 public 接口方法
- 异常被 catch 吞掉漏了:事务拦截器看不到异常,事务不会回滚 —— 这是工程中最常见的事务"静默失效"
- 传播机制选错漏了:如 REQUIRES_NEW 导致外层事务看不到内层改动
- @EnableTransactionManagement 未开启漏了
- 数据库引擎不支持事务漏了:如 MySQL 用 MyISAM
- @Transactional 加在字段/构造器上漏了:只能加在类或方法上
补充(事务失效 10 条完整清单):
| # | 失效原因 | 排查点 |
|---|
| 1 | 类未注册为 Spring Bean | 没有 @Service/@Component 或被 @Configuration 扫描 |
| 2 | 通过 new 直接实例化 | 必须通过容器 @Autowired 注入 |
| 3 | 类内部自调用(this.method()) | 走 this 不走代理,最常见 |
| 4 | 方法 private / final / static | 代理无法覆写 |
| 5 | 方法非 public | JDK 动态代理限制 |
| 6 | 异常被 catch 吞掉 | 事务拦截器看不到异常 |
| 7 | 传播机制不匹配(如 REQUIRES_NEW) | 业务期望与实际传播行为不符 |
| 8 | 数据库本身不支持事务 | MySQL 用 MyISAM |
| 9 | @EnableTransactionManagement 未开启 | Spring 原生项目需要,Spring Boot 自动配置 |
| 10 | 注解加在字段/构造器上 | 只能加在类或方法上 |
加分项:
- @Transactional 加在类上会应用到所有 public 方法,可能带来性能损耗
- @Transactional(readOnly=true) 优化:让数据库连接用只读、跳过脏检查
- 传播机制组合:REQUIRED + REQUIRES_NEW 实现"外层大事务 + 内层独立提交"
完整讲解
第一部分:知识重点剖析
核心考点定位:考察对 Spring 事务失效场景的系统性掌握和工程排查能力。
知识点拆解:
- 失效的两大根本原因:
- 前提不成立:数据库层不支持事务、@EnableTransactionManagement 未开启
- 代理机制失效:所有其他失效场景(10 条清单里 8 条)都源于"调用没有走代理对象"
- 完整失效清单(10 条):
- 类未注册为 Spring Bean:没有 @Service/@Component 或被 @Configuration 扫描
- 通过 new 直接实例化:必须通过容器 @Autowired 注入
- 类内部自调用(this.method()):走 this 不走代理,最常见
- 方法 private / final / static:代理无法覆写
- 方法非 public:JDK 动态代理限制
- 异常被 catch 吞掉:事务拦截器看不到异常
- 传播机制不匹配(如 REQUIRES_NEW):业务期望与实际传播行为不符
- 数据库本身不支持事务:MySQL 用 MyISAM
- @EnableTransactionManagement 未开启:Spring 原生项目需要,Spring Boot 自动配置
- 注解加在字段/构造器上:只能加在类或方法上
- 常见自调用规避方案:
- 从其他 Bean 中调用
- AopContext.currentProxy() 获取当前代理对象
- @Autowired @Lazy private MyService self 自注入,然后 self.xxx()
- 拆成两个 Bean(架构最佳)
- 异常处理的两大坑:
- 默认只回滚 RuntimeException 和 Error,受检异常不回滚
- 异常被 catch 吞掉:拦截器在 finally 之前看不到异常,事务按成功提交
- 建议:@Transactional(rollbackFor = Exception.class) + 不要静默 catch
- 传播机制选错的典型场景:
- REQUIRES_NEW:内层事务和外层事务独立提交,外层回滚不影响内层
- NESTED:使用 savepoint 嵌套,外层回滚会回滚所有嵌套
- REQUIRED:默认值,加入外层事务,但可能出现"外层已提交,内层回滚导致数据不一致"
- 加分项:
- @Transactional 加在类上会应用到所有 public 方法,可能带来性能损耗
- @Transactional(readOnly = true) 让数据库连接用只读、跳过脏检查
- 传播机制组合:REQUIRED + REQUIRES_NEW 实现"外层大事务 + 内层独立提交",用于审计日志等必须提交场景
第二部分:考察方向与延伸递进
考察方向:
- 深度:代理机制原理、AOP 织入时机
- 广度:数据库事务支持、传播机制、异常传播链
- 应用:生产环境事务失效排查、日志追踪
延伸递进面试题:
- 生产上如何快速定位 @Transactional 是否真的生效了?
- 解答要点:三种方式——① 在方法内调 Thread.currentThread().getStackTrace() 检查调用栈是否经过 TransactionInterceptor 代理对象;② 打印目标对象的 class 名,看是否包含 $$EnhancedBySpringCGLIB 或 $$ 后缀(CGLIB 代理标志);③ 打开 Spring 事务调试日志(logging.level.org.springframework.transaction=DEBUG)观察事务开启/提交日志
- 如何用事务事件(TransactionalEventListener)把非事务代码也挂到事务的生命周期?
- 解答要点:用 @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT / AFTER_COMMIT / AFTER_ROLLBACK) 监听事务生命周期,在发布端通过 ApplicationEventPublisher.publishEvent() 发送事件;适合"事务提交后再执行非事务操作"的场景,如发消息、发邮件、更新缓存
- @Transactional 里 catch 异常然后手动回滚怎么做?抛事务异常和 catch 后调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 有什么区别?
- 解答要点:直接抛异常最干净,事务拦截器自动 rollback;catch 后手动 setRollbackOnly 只是标记回滚,但业务逻辑可能还继续执行了后续代码,容易漏出脏数据;最佳实践是 catch 后 rethrow(throw new RuntimeException(e)),让事务拦截器自然处理
第三部分:面试者参考回答(300-600 字口语化)
面试官您好,Spring 事务失效的场景我一般分成两大类来记:
第一类是前提不成立:
- 数据库本身不支持事务(比如 MySQL 用 MyISAM 引擎)
- @EnableTransactionManagement 没开启(Spring 原生项目常见,Spring Boot 自动配置所以不常踩)
- @Transactional 注解加在字段或构造器上(只能加在类或方法上)
第二类是代理机制失效,这是 90% 的失效场景都在这类里。Spring 事务基于 AOP 代理实现,所以只要调用没走代理对象,事务就一定失效。具体的场景我数一下有 8 条:
- 类没注册成 Spring Bean:没有 @Service/@Component 标注,或 @ComponentScan 没扫到,Spring 不会给这类建代理
- 通过 new 直接实例化:绕过 Spring 容器,拿到的肯定是原始对象
- 类内部自调用(this.method()):这是最经典的坑。this 是原始对象,不走代理,事务直接失效。规避方案有四种:从其他 Bean 调用、AopContext.currentProxy()、@Lazy 自注入 self 字段、或拆成两个 Bean
- 方法 private / final / static:CGLIB 无法覆写 final 和 static,也无法拦截 private 方法
- 方法非 public:JDK 动态代理只支持 public 接口方法
- 异常被 catch 吞掉:事务拦截器在 finally 之前看不到异常,事务按成功提交,这是最隐蔽的"静默失效"
- 传播机制选错:比如 REQUIRES_NEW 会挂起外层事务,内层回滚不影响外层,业务以为整个都回滚了但其实内层已提交
- 数据库引擎不支持事务:这个虽然归在前提类,但线上真会遇到"MySQL 建表没指定引擎默认用 MyISAM"
几个补充建议:
- @Transactional 加在类上会应用到所有 public 方法,性能有损耗,能精确到方法就精确到方法
- 建议显式加
@Transactional(rollbackFor = Exception.class) 避免受检异常不回滚
- 建议
@Transactional(readOnly = true) 用在查询方法上,数据库连接会走只读模式跳过脏检查
- 生产上定位事务是否生效可以用三个方法:看目标对象 class 名是否包含
$$EnhancedBySpringCGLIB、用 Thread.currentThread().getStackTrace() 检查调用栈、或打开 logging.level.org.springframework.transaction=DEBUG 日志
实际工程中的最佳实践:
- 事务边界划在 Service 层,不要在 DAO 层加 @Transactional
- 不要把事务方法设计得太大,事务内不要做 RPC 调用、发消息、发邮件这些非事务操作
- 必须的非事务副作用用 @TransactionalEventListener 挂到事务提交之后