@Transactional 看起来简单,但事务失效是项目里最常见的坑之一。要判断事务为什么没回滚,先抓住一个核心:Spring 声明式事务依赖代理和异常传播。代理没生效,事务不会进;异常没抛出去,事务不知道要回滚。

在这里插入图片描述

Transactional
方法被调用

是否经过
Spring 代理

事务不会生效

事务拦截器
开启事务

执行业务代码

异常是否
抛到代理层

代理认为成功
直接提交

异常是否符合
回滚规则

默认不回滚

回滚事务

场景一:异常被自己捕获了

业务方法执行

数据库扣减余额

发生异常

catch 捕获异常

是否重新抛出

事务代理
感知不到异常

提交事务

事务代理
捕获异常

回滚事务

事务通知要感知到目标方法抛出的异常,才能触发回滚。如果业务代码自己把异常吃掉了,外层事务代理就会认为方法正常结束,于是提交事务。

@Transactional
public void transfer(Integer from, Integer to, BigDecimal money) {
    try {
        accountMapper.decrease(from, money);
        int i = 1 / 0;
        accountMapper.increase(to, money);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

这段代码的问题是 catch 之后没有继续抛异常。事务代理看不到异常,就不会回滚。

正确处理方式:

@Transactional
public void transfer(Integer from, Integer to, BigDecimal money) {
    try {
        accountMapper.decrease(from, money);
        int i = 1 / 0;
        accountMapper.increase(to, money);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

或者直接不捕获,让异常自然抛出。

场景二:抛出了检查异常

Spring 默认只对 RuntimeExceptionError 回滚。像 IOExceptionFileNotFoundException 这类检查异常,默认不会触发回滚。

@Transactional
public void importUser() throws FileNotFoundException {
    userMapper.insert(user);
    new FileInputStream("not-exist.txt");
}

如果希望检查异常也回滚,需要配置 rollbackFor

@Transactional(rollbackFor = Exception.class)
public void importUser() throws FileNotFoundException {
    userMapper.insert(user);
    new FileInputStream("not-exist.txt");
}

项目里建议直接写成:

@Transactional(rollbackFor = Exception.class)

这样比依赖默认规则更清楚。

场景三:方法不是 public

Spring 为方法创建事务代理时,通常要求被增强的方法是 public。如果事务方法是默认访问级别、protectedprivate,事务可能不会按预期生效。

@Transactional(rollbackFor = Exception.class)
void updateAccount() {
    accountMapper.update(account);
}

应该改成:

@Transactional(rollbackFor = Exception.class)
public void updateAccount() {
    accountMapper.update(account);
}

这类问题非常隐蔽,因为代码能运行,但事务边界没有真正按你想的方式进入代理逻辑。

场景四:同类内部方法调用

这属于面试和项目里都很常见的扩展坑。Spring 事务通过代理对象生效,如果一个类内部直接用 this 调另一个带事务的方法,就绕过了代理。

@Service
public class OrderService {

    public void createOrder() {
        this.saveOrder();
    }

    @Transactional(rollbackFor = Exception.class)
    public void saveOrder() {
        orderMapper.insert(order);
    }
}

createOrder() 内部调用 saveOrder(),没有经过 Spring 代理对象,事务可能不生效。

常见处理方式:

  1. 把事务方法拆到另一个 Service 中,通过 Spring 注入后调用。
  2. 让外层入口方法加事务。
  3. 必要时通过代理对象调用,但这通常不如拆分职责清楚。

场景五:数据库本身不支持事务

如果底层存储引擎不支持事务,Spring 再怎么加事务也没用。比如 MySQL 的 InnoDB 支持事务,MyISAM 不支持事务。

这类问题不一定是代码问题,而是表结构或数据库引擎选择问题。

快速排查清单

排查点 怎么判断
注解是否加在 public 方法上 检查方法访问修饰符
异常是否被吞掉 catch 后有没有重新抛出
异常类型是否会回滚 检查是否配置 rollbackFor
是否同类内部调用 看调用链有没有经过 Spring 代理
数据库是否支持事务 检查存储引擎和连接配置
方法是否由 Spring 容器管理 检查对象是不是自己 new 出来的

面试回答模板

可以这样回答:

Spring 事务失效常见原因有几个。第一,异常被业务代码捕获后没有继续抛出,事务代理感知不到异常,所以不会回滚。第二,抛出的是检查异常,Spring 默认只回滚运行时异常,需要配置 rollbackFor = Exception.class。第三,事务方法不是 public,代理增强可能不生效。第四,同一个类内部方法调用会绕过代理对象。还有一些场景比如数据库引擎不支持事务,或者对象不是由 Spring 容器管理,也会导致事务不生效。

小结

排查事务失效,不要只盯着 @Transactional 有没有写。

真正要看两条线:

  1. 调用有没有经过 Spring 代理。
  2. 异常有没有按回滚规则抛到事务代理。

只要这两条线断了,事务就可能失效。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐