【大白话说Java面试题 第160题】【06_Spring篇】第20题:Spring 事务什么时候会失效?

📌 PDF :大白话说Java面试题 --- 06_Spring篇

第20题:Spring 事务什么时候会失效?

📚 回答:

  • 核心考点Spring 事务失效 是面试中最高频的考点之一,大厂面试官不会满足于背诵"非 public、同类调用"等表面原因,而是期望你深入理解 AOP 代理机制的本质 (JDK 动态代理 vs CGLIB 的字节码重写边界)、事务拦截的完整链路 (从 TransactionInterceptorThreadLocal 绑定)、异常回滚的精确语义 (默认仅回滚 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);
        }
    }

    解决方案 :将方法改为 publiccitation: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

  • 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 的默认回滚规则只针对 RuntimeExceptionError。checked Exception(如 IOExceptionSQLException)默认不会触发回滚。

    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 多数据源未指定事务管理器

    原因 :当存在多个 PlatformTransactionManager Bean 时,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() 中定义:只回滚 RuntimeExceptionError。这是设计选择------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 代理机制被打破事务拦截链路被绕过。理解失效场景必须从源码级抓住三个核心:

  1. 代理是前提 :只有经过代理对象调用的 public 实例方法才会被 TransactionInterceptor 拦截。同类自调用(this)、final/static 方法、非容器管理的 Bean 都绕过了代理。
  2. 异常是信号TransactionInterceptor 通过捕获异常触发回滚。异常被 try-catch 吞掉、checked 异常默认不回滚、错误配置 noRollbackFor 都会导致"该回滚的没回滚"。
  3. 上下文是纽带 :事务通过 ThreadLocal 绑定连接,@Async 新线程会丢失上下文;多数据源需要显式指定事务管理器;数据库引擎(MyISAM)不支持事务则 Spring 事务完全无效。

生产环境排查事务问题,记住四步法:确认代理生效 → 确认事务属性解析 → 确认异常传播到拦截器 → 确认数据库层面支持。同时开启 DEBUG 日志是最有效的定位手段。最后,同类自调用和异常被吞掉是实际开发中最隐蔽、最高频的两个坑,必须形成肌肉记忆。


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
我今晚不熬夜2 小时前
Java语言实现Modbus协议通过用串口读取数据(以RTU为例)
java·网络协议
小锋java12343 小时前
Maven 4 要来了:15 年后,Java 构建工具迎来"彻底重构"
java·maven
卓怡学长3 小时前
w269基于spring boot + vue 候鸟监测数据管理系统
java·数据库·spring boot·spring·intellij-idea
孫治AllenSun4 小时前
【MDC】使用 MDC 封装一个 LogMdcUtil 工具类
java·spring boot·后端
济*沧*海4 小时前
BaseMapper的介绍
java·windows·tomcat
plainGeekDev4 小时前
依赖管理 → Version Catalog
android·java·kotlin
淘源码d5 小时前
医学影像云PACS系统——PACS诊断工作站
java·源码·影像处理·医院管理系统·影像工作站·云pacs系统·dicom 标准通信
KobeSacre6 小时前
Semaphore 源码
java·开发语言
NiceCloud喜云6 小时前
Anthropic 一周三连发:Cowork 多端、Fable 5 按需付费、J-space 论文的技术解读
java·服务器·网络·人工智能·ai
SamDeepThinking6 小时前
组合还是继承?我在项目里主要看这两个判断
java·后端·面试