Spring 事务失效可能是哪些原因?


面试考察点

  1. AOP 原理理解:面试官不仅仅是想让你罗列几个场景,更是想知道你是否真正理解 Spring 事务基于 AOP 动态代理的底层机制。大部分事务失效场景的根因都在这里。
  2. 实战踩坑经验:考察你在真实项目中是否遇到过事务失效的问题,以及排查思路。只会背八股和真正踩过坑的人,回答的深度完全不一样。
  3. 编码规范意识:看你是否了解 Spring 事务使用的最佳实践,能不能在编码阶段就规避这些坑。

核心答案

Spring 事务失效的常见原因,我按 "出现频率" 从高到低梳理了 7 大类

序号失效场景根本原因严重程度
1同一个类内部方法调用未经过代理对象,AOP 不生效⭐⭐⭐⭐⭐
2方法不是 publicSpring AOP 默认只代理 public 方法⭐⭐⭐⭐⭐
3方法被 finalstatic 修饰无法被代理类重写⭐⭐⭐⭐
4异常被 try-catch 吞掉代理对象感知不到异常,不会触发回滚⭐⭐⭐⭐⭐
5抛出的异常类型不对默认只回滚 RuntimeException,不回滚受检异常⭐⭐⭐⭐
6数据库引擎不支持事务比如 MySQL 的 MyISAM 引擎⭐⭐⭐
7事务传播行为配置错误比如 NOT_SUPPORTEDNEVER 本身就不走事务⭐⭐⭐

深度解析

一、同类内部方法调用(最常见!)

这是事务失效的 "头号杀手"。看下面这段代码:

@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 创建代理时,会检查目标方法的访问修饰符。privateprotected、包级可见的方法,事务注解直接被忽略

@Service
public class OrderService {

    // ❌ 事务不生效!
    @Transactional
    private void createOrder() { ... }

    // ❌ 事务不生效!
    @Transactional
    protected void updateOrder() { ... }

    // ✅ 事务生效
    @Transactional
    public void deleteOrder() { ... }
}

源码层面,SpringTransactionAnnotationParser 解析事务注解时,AbstractFallbackTransactionAttributeSource 会在 computeTransactionAttribute() 方法中做访问修饰符的校验,非 public 方法直接返回 null,不创建事务属性。

三、方法被 finalstatic 修饰

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 事务默认 只对 RuntimeExceptionError 回滚,不对受检异常(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;

七、事务传播行为配置错误

@Transactionalpropagation 参数如果不小心配错了,事务也会 "失效":

// 以非事务方式执行,如果当前存在事务则挂起
@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 而失败。

面试高频追问

  1. 追问一:Spring 事务的底层实现原理是什么?

    • 基于 AOP 动态代理。通过 TransactionInterceptor 拦截 @Transactional 方法,在方法执行前开启事务,执行后根据异常情况决定提交或回滚。核心类是 AbstractPlatformTransactionManager
  2. 追问二:@TransactionalrollbackFor 属性默认值是什么?

    • 默认只回滚 RuntimeExceptionError。如果需要回滚受检异常,必须显式指定 rollbackFor = Exception.class
  3. 追问三:Spring 事务的传播行为有哪些?常用的有哪几个?

    • 7 种传播行为。最常用的是 REQUIRED(默认,有事务就加入,没有就新建)和 REQUIRES_NEW(总是新建事务,挂起当前事务)。
  4. 追问四:如何在同一个类中让内部方法调用也走事务?

    • 注入自身代理对象(@Autowired 自身)、从 ApplicationContext 获取代理对象、使用 AopContext.currentProxy()

常见面试变体

  • "说说 @Transactional 注解有哪些坑?"
  • "Spring 事务在什么情况下不会回滚?"
  • "为什么 Spring 事务失效?你是怎么排查的?"
  • "@TransactionalrollbackFor 属性有什么用?"

记忆口诀

私(非 public)静(static)终(final)内(同类调用)吞(异常被 catch)错(异常类型不对)引(引擎不支持)

翻译一下:非 public、static、final、同类内部调用、异常被吞、异常类型错、引擎不支持事务。前四个都是 "代理不生效",后三个是 "事务不回滚"。

总结

Spring 事务失效的根因就两个方向:代理没生效(同类调用、非 public、final/static、未被 Spring 管理)和事务没回滚(异常被吞、异常类型不对、引擎不支持、传播行为配错)。排查的时候沿着这条线逐一排查,基本不会漏。