Spring 事务失效的 8 个场景,每一个都踩过

@Transactional 在同类方法调用、非 public 方法、异常被吞掉、多数据源等 8 种场景下会悄悄失效。每种给出复现代码、失效原因和修法。

"我加了 @Transactional,为什么还是没回滚?"

这是 Spring 里被问得最多的问题之一。答案往往是这句话:@Transactional 不是"打开事务"的开关,它只是给方法套了一层代理。 代理绕过去的地方,事务就不存在。

先把这个前提讲透,后面 8 个场景就好理解了。

一、事务到底是怎么加上去的

Spring 的事务基于 AOP 动态代理。你写:

@Service
public class OrderService {
    @Transactional
    public void createOrder(Order order) { ... }
}

Spring 启动时给 OrderService 生成了一个代理对象,容器里装的其实是这个代理。外部调用 orderService.createOrder() 时,实际执行的是代理的 createOrder:

调用方
  -> 代理对象
       |-- 1. 从 DataSource 拿连接,关闭自动提交
       |-- 2. 执行业务方法(真实对象)
       |-- 3. 正常返回 -> commit
       |-- 4. 抛异常   -> rollback
  -> 真实对象的方法体

因此有一条铁律:只有经过代理的调用,事务才生效。 剩下的 8 个场景,本质上都是"代理被绕过"或者"回滚条件不满足"。

场景 1:同类内部调用(最高频)

@Service
public class OrderService {

    public void outer() {
        // 这是 this.inner(),绕过了代理
        this.inner();
    }

    @Transactional
    public void inner() {
        // 这里的事务不生效
        orderMapper.insert(...);   // 抛异常也不会回滚
    }
}

this.inner() 调用的是真实对象自己,代理完全没有参与。所有方法级别的切面在这个瞬间全部失效。

修复三选一:

最干净的是拆分职责,让调用跨类:

@Service
public class OrderService {
    @Resource private OrderInnerService innerService;

    public void outer() {
        innerService.inner();   // 走代理
    }
}

其次可以自注入代理:

@Service
public class OrderService {
    @Resource @Lazy private OrderService self;   // @Lazy 避免循环依赖

    public void outer() {
        self.inner();
    }
}

或者用 AopContext.currentProxy()(需要在启动类加 @EnableAspectJAutoProxy(exposeProxy = true),性能略差,不推荐)。

注意 @Lazy 那个写法实际拿到的是代理对象,不是真实对象——这也是为什么它能生效。

场景 2:方法不是 public

Spring 5.x 的 TransactionInterceptor 在处理时,对非 public 方法默认不拦:

@Transactional
private void doSomething() { ... }     // 不生效

@Transactional
protected void doOther() { ... }       // 不生效(CGLIB 下也是)

@Transactional
void packageMethod() { ... }           // 不生效

原因在 AbstractFallbackTransactionAttributeSource.computeTransactionAttribute:

// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
    return null;
}

修复:改成 public。 如果确实需要 private 的逻辑带事务,说明方法边界划错了,应该提取到独立的类里。

补充一句:Spring 6 / Boot 3 之后对 protected 和包级方法有放宽(ProxyTransactionManagementConfiguration 的行为变了),但依赖这个特性会让代码在 5.x 和 6.x 之间行为不一致,不如统一用 public。

场景 3:异常被自己吞掉了

@Transactional
public void createOrder(Order order) {
    try {
        orderMapper.insert(order);
        int a = 1 / 0;
    } catch (Exception e) {
        log.error("下单失败", e);
        // 异常被吃掉了,代理看不到,认为方法正常返回 -> commit
    }
}

事务管理器靠异常传播到代理层来判断要不要回滚。catch 住不抛,代理收到的是正常返回,直接 commit,数据就脏了。

修复:要么重新抛,要么手动标记回滚。

@Transactional
public void createOrder(Order order) {
    try {
        orderMapper.insert(order);
        int a = 1 / 0;
    } catch (Exception e) {
        log.error("下单失败", e);
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        throw new BizException("下单失败", e);   // 推荐:让调用方知道失败了
    }
}

只调用 setRollbackOnly() 不抛异常,事务会回滚但调用方拿到的是成功——这个坑比不回滚更隐蔽,因为它静默失败。

场景 4:抛的是检查异常

@Transactional 的默认 rollbackFor 是 RuntimeException 和 Error,不包括 Exception 及其检查子类:

@Transactional
public void importData() throws IOException {
    // 抛 IOException 时不会回滚
    Files.readAllBytes(path);      // IOException
    mapper.insert(...);            // 已经写进去了
}

修复:显式声明。

@Transactional(rollbackFor = Exception.class)

这条建议值得无脑加在所有业务方法上。约定优于配置在这里反而危险——没人会记得"我抛的是检查异常"。

场景 5:类没被 Spring 管理

// 没有 @Service / @Component
public class OrderService {
    @Transactional
    public void create() { ... }
}

// 调用方
OrderService s = new OrderService();   // 自己 new 的,没有代理
s.create();                            // 事务不生效

你 new 出来的对象,Spring 一无所知,自然也不会给它套代理。

同类问题还有:

  • 用了 @Component 但所在包不在 @ComponentScan 范围内
  • 注解加在接口上而代理方式是 CGLIB 且 proxyTargetClass=true(部分版本会丢注解)
  • 把 @Transactional 加在 @Bean 方法返回的 new 对象上

修复:确认对象出自 Spring 容器。 快速验证方法是在类里注入 ApplicationContext,看 ctx.getBean(OrderService.class) 拿到的是不是 OrderService$$EnhancerBySpringCGLIB$$xxx。

场景 6:多线程与异步

@Transactional
public void batchCreate(List<Order> orders) {
    orders.forEach(o -> executor.submit(() -> {
        // 子线程里,事务上下文(ThreadLocal)已经丢了
        orderMapper.insert(o);     // 独立自动提交,外部事务回滚不了它
    }));
}

Spring 的事务上下文存在 ThreadLocal 里。跨线程就断了,子线程的操作走自己的连接、自己提交。

修复:让每个子任务自己开事务,并且由外部统一收集异常。

@Service
public class OrderBatchService {
    @Resource private OrderService orderService;

    public void batchCreate(List<Order> orders) throws Exception {
        List<Future<?>> futures = new ArrayList<>();
        for (Order o : orders) {
            futures.add(executor.submit(() -> orderService.createOne(o)));
        }
        for (Future<?> f : futures) {
            f.get();     // 有异常就会抛出来,触发补偿逻辑
        }
    }
}

@Service
class OrderService {
    @Transactional(rollbackFor = Exception.class)
    public void createOne(Order o) { orderMapper.insert(o); }
}

注意这样不是原子操作——部分成功部分失败。真正的分布式一致性要靠补偿(Saga/TCC)或者先落中间表再异步处理。

@Async 同理:@Async + @Transactional 加在同一个方法上,事务范围只包住"提交任务"这一步,异步线程里的操作不受控。

场景 7:传播行为设错

@Transactional(propagation = ...) 的 7 种取值里,最容易被误用的是这三个:

REQUIRES_NEW:挂起当前事务,新开一个独立事务,独立提交独立回滚。

@Transactional
public void placeOrder() {
    orderMapper.insert(o);          // 事务 A
    logService.writeLog();          // REQUIRES_NEW -> 事务 B,自己提交
    throw new RuntimeException();   // A 回滚,但 B 已提交,日志留下了
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeLog() { ... }

这不是 bug,是设计意图。但如果你的预期是"要么全成功要么全失败",那就是用错了。

NOT_SUPPORTED:挂起事务,以非事务方式执行。常见误区是拿它当"只读加速"用,实际它完全不参与事务,里面写的操作无法回滚。

SUPPORTS:有事务就加入,没有就以非事务执行。在外层没开事务的情况下被调用,里面的操作是没有事务保护的——这常常出现在"某个方法被两个入口调用,一个开事务一个没开"的场景里。

场景 8:数据库引擎不支持

SHOW TABLE STATUS LIKE 'orders';
-- Engine: MyISAM   <- 不支持事务

MySQL 5.5 之后默认是 InnoDB,但老库迁移过来的表、或者建表时手写了 ENGINE=MyISAM,就会静默失败:@Transactional 正常执行、正常 commit,但回滚时数据纹丝不动。

修复:

ALTER TABLE orders ENGINE = InnoDB;

附:两个不在"失效"范畴但同样致命的问题

事务里做远程调用。 事务开着的时候去调第三方 HTTP 接口,连接池被占住,接口一慢就把数据库连接池打满:

@Transactional
public void createOrder() {
    orderMapper.insert(o);
    httpClient.post("https://third-party/notify");   // 网络抖动 5 秒 = 连接被占 5 秒
}

正确姿势是把远程调用移出事务边界:先落库提交,再用事件或消息队列异步通知。

事务里做大批量操作。 一条 @Transactional 包住 10 万行 update,会产生巨大的 undo log,长事务还会阻塞其他会话。分批提交更稳。

自检清单

排查"事务不生效"时按顺序过一遍,能覆盖 95% 的情况:

  1. 对象是不是从容器里拿的(不是 new 的)
  2. 调用是不是经过了代理(不是 this. 自调用)
  3. 方法是不是 public
  4. 异常有没有被 catch 掉
  5. 抛的是不是 RuntimeException(检查异常需要 rollbackFor)
  6. 是不是跨线程/异步了
  7. 传播行为是不是 REQUIRES_NEW / NOT_SUPPORTED(独立提交,回滚不了)
  8. 表引擎是不是 InnoDB

快速验证是否真的开了事务,最直接的办法是打开 SQL 日志看有没有 SET autocommit=0:

logging:
  level:
    org.springframework.jdbc.datasource.DataSourceTransactionManager: DEBUG
    org.mybatis.spring.transaction: DEBUG

如果这行日志压根没出现,说明事务根本没开始,直接跳到第 1、2、3 条去查。

查看「Java 与 JVM」全部文章 →

同分类文章

查看 0 评论 0

评论

正在加载评论…

留言提交后自动审核:正常内容立即显示,含敏感信息的会转人工复核。