Spring 事务为什么会失效?结合真实代码讲清几个常见坑

摘要

很多人都遇到过这种情况:代码明明加了 @Transactional,但数据还是提交了,或者异常发生后并没有按预期回滚。本文结合真实开发中最常见的几个场景,梳理 Spring 事务失效的典型原因,包括同类方法内部调用、异常被吞掉、非运行时异常不回滚、方法访问权限问题以及异步场景下事务边界变化等,并给出对应的正确写法和排查思路。

前言

在日常开发里,Spring 事务几乎是最常用的能力之一。

很多业务代码都会这么写:

csharp 复制代码
@Transactional(rollbackFor = Exception.class)
public void doBusiness() {
    // 业务处理
}

看起来很简单,好像只要加上这个注解,事务问题就解决了。

但真实开发里,事务相关的问题其实非常多,最常见的一类就是:

代码明明加了 @Transactional,但事务并没有按预期生效。

这种问题很容易让人误判,因为表面上看代码没问题,日志里也未必直接报错,但结果就是:

  • 该回滚的时候没回滚
  • 该整体成功的时候只成功了一部分
  • 事务边界和自己想的不一样

这篇文章我就结合几个很常见的场景,整理一下 Spring 事务为什么会失效,以及实际开发中该怎么避免这些坑。

一、为什么会感觉事务"加了但没生效"

很多人第一次遇到事务问题时,都会有一种感觉:

我都已经加注解了,为什么还是不对?

原因就在于,Spring 事务并不是"看到注解就一定生效",它本质上依赖的是 AOP 代理机制

也就是说,事务能否生效,和下面这些因素都有关系:

  • 方法是不是通过 Spring 代理对象调用的
  • 异常有没有真正抛出去
  • 抛出的异常类型是不是默认支持回滚
  • 方法访问权限是否符合代理要求
  • 当前执行线程是不是还在原来的事务上下文里

所以事务问题很多时候并不是"注解没写",而是:

事务的触发条件和你的实际调用方式不一致。

二、同类方法内部调用,事务为什么会失效

这是最经典、也最容易踩的坑。

错误写法

typescript 复制代码
@Service
public class OrderService {

    public void createOrder() {
        saveMainOrder();
    }

    @Transactional(rollbackFor = Exception.class)
    public void saveMainOrder() {
        // 保存主单
        // 保存明细
        throw new RuntimeException("模拟异常");
    }
}

很多人会觉得,saveMainOrder() 已经加了事务,执行异常应该回滚。

但如果 createOrder() 是在当前类内部直接调用 saveMainOrder(),事务很可能不会生效。

为什么会失效

因为 Spring 事务依赖代理对象。

而同类内部调用,本质上是 this.saveMainOrder(),没有经过 Spring 代理,自然也就绕过了事务增强。

正确写法一:把事务方法拆到另一个 Bean

typescript 复制代码
@Service
public class OrderService {

    @Resource
    private OrderTxService orderTxService;

    public void createOrder() {
        orderTxService.saveMainOrder();
    }
}

@Service
public class OrderTxService {

    @Transactional(rollbackFor = Exception.class)
    public void saveMainOrder() {
        // 保存主单
        // 保存明细
        throw new RuntimeException("模拟异常");
    }
}

正确写法二:通过代理对象调用

这种方式能用,但不如拆 Bean 清晰,日常开发里我更推荐第一种。

一句话总结

同类内部直接调用,事务不会经过 Spring 代理,注解等于白加。

三、异常被捕获但没有继续抛出,为什么不会回滚

这个场景也特别常见。

错误写法

csharp 复制代码
@Transactional(rollbackFor = Exception.class)
public void updateData() {
    try {
        // 更新表A
        // 更新表B
        int i = 1 / 0;
    } catch (Exception e) {
        log.error("执行异常", e);
    }
}

很多人会以为这里出异常了,事务就会自动回滚。

其实不一定。

为什么会失效

Spring 事务默认是通过"方法抛出异常"来感知是否需要回滚的。

如果你把异常 catch 住了,又没有继续往外抛,那对 Spring 来说,这个方法就是"正常结束"的,它自然会提交事务。

正确写法一:捕获后重新抛出

php 复制代码
@Transactional(rollbackFor = Exception.class)
public void updateData() {
    try {
        // 更新表A
        // 更新表B
        int i = 1 / 0;
    } catch (Exception e) {
        log.error("执行异常", e);
        throw new RuntimeException(e);
    }
}

正确写法二:手动标记回滚

csharp 复制代码
@Transactional(rollbackFor = Exception.class)
public void updateData() {
    try {
        // 更新表A
        // 更新表B
        int i = 1 / 0;
    } catch (Exception e) {
        log.error("执行异常", e);
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

一句话总结

异常被吞掉,Spring 感知不到失败,事务通常不会回滚。

四、抛出的不是运行时异常,默认为什么不回滚

这个坑很多人也是在踩过之后才知道。

错误写法

java 复制代码
@Transactional
public void handleData() throws Exception {
    // 业务处理
    throw new Exception("模拟受检异常");
}

很多人会觉得这里抛异常了,事务应该回滚。

但默认情况下,不一定。

为什么会失效

Spring 默认只会对:

  • RuntimeException
  • Error

进行回滚。

像 Exception 这种受检异常,如果你没有显式指定,默认是不回滚的。

正确写法

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void handleData() throws Exception {
    // 业务处理
    throw new Exception("模拟受检异常");
}

一句话总结

默认回滚的是运行时异常,不是所有异常。

五、方法不是 public,事务为什么可能不生效

这个坑属于不太起眼,但也确实会遇到(在 IntelliJ IDEA 这类的工具中,会有明显的编译错误)。

错误写法

csharp 复制代码
@Transactional(rollbackFor = Exception.class)
private void doUpdate() {
    // 业务处理
}

或者:

csharp 复制代码
@Transactional(rollbackFor = Exception.class)
protected void doUpdate() {
    // 业务处理
}

为什么会失效

在常见的 Spring 代理模式下,事务增强一般是针对 public 方法生效的。

如果方法不是 public,很多场景下事务代理根本不会生效。

正确写法

csharp 复制代码
@Transactional(rollbackFor = Exception.class)
public void doUpdate() {
    // 业务处理
}

一句话总结

事务方法尽量用 public,不要把关键事务逻辑放在 private 方法上。

六、异步或多线程场景下,事务边界为什么会变

这个坑在复杂业务里特别容易被忽略。

典型场景

csharp 复制代码
@Transactional(rollbackFor = Exception.class)
public void process() {
    // 主线程更新数据

    CompletableFuture.runAsync(() -> {
        // 子线程更新数据
    });

    throw new RuntimeException("模拟异常");
}

很多人会以为,外层方法有事务,里面异步线程的操作也应该跟着一起回滚。

实际上通常不是这样。

为什么会失效

Spring 事务是和当前线程绑定的。

你在主线程里开启的事务,并不会自动传播到新的异步线程里。

也就是说:

  • 主线程有主线程的事务上下文
  • 子线程是新的执行线程
  • 子线程里的数据库操作,默认不在主线程事务里

所以你主线程抛异常回滚,并不代表子线程里的操作也会跟着回滚。

正确思路

异步场景下不要想当然地把它当成"还是同一个事务"。

通常应该:

  • 明确主线程和子线程的事务边界
  • 子线程里如果有独立数据库操作,需要自己定义事务策略
  • 对一致性要求高的场景,不要轻易混用事务和异步

一句话总结

Spring 事务默认是线程级别的,跨线程后事务上下文就变了。

七、真实开发里,我一般怎么排查事务问题

事务问题有时候比普通 bug 更绕,因为代码表面上看不一定有错。

我一般会按下面几个方向排查。

1. 先看调用方式有没有经过 Spring 代理

先确认是不是:

  • 同类内部调用
  • this.xxx() 调用
  • 非 Spring 管理对象调用

这类问题出现概率非常高。

2. 再看异常有没有真正抛出去

重点确认:

  • 是不是 try-catch 后吞掉了
  • 有没有只是打日志没抛异常
  • 有没有手动 return 掉

3. 看异常类型是否支持默认回滚

如果抛的是受检异常,要确认有没有写:

python 复制代码
@Transactional(rollbackFor = Exception.class)

4. 看执行线程有没有变化

如果中间有这些场景,要特别小心:

  • @Async
  • CompletableFuture
  • 线程池
  • 消息异步消费

很多事务问题本质上不是"事务失效",而是"你已经不在原来的事务线程里了"。

5. 最后看日志和数据库结果

事务问题不能只看代码推断,最好结合:

  • SQL 执行日志
  • 异常日志
  • 数据库最终结果

因为事务最终有没有生效,还是要看数据结果。

八、我自己总结的几个经验

1. 不要把事务想得太"自动化"

很多人会下意识觉得,只要加了 @Transactional,Spring 就会自动处理好一切。

其实不是。

事务生效有前提,回滚也有条件。

2. 事务代码要尽量清晰,不要写得太绕

事务方法里如果:

  • 又有内部调用
  • 又有异步
  • 又有异常吞掉
  • 又有多层封装

那后期出问题会很难查。

3. 对一致性要求高的代码,最好显式设计事务边界

不要把"事务应该会生效吧"当成理所当然。

重要链路最好一开始就把事务边界想清楚。

4. 真正判断事务有没有生效,最终还是看数据

代码只是"理论",数据库结果才是"事实"。

总结

Spring 事务本身并不复杂,真正复杂的是:

业务代码的调用方式、异常处理方式以及执行线程,往往会让事务边界和想象中不一致。

这篇文章总结了几个最常见的事务失效场景:

  • 同类方法内部调用
  • 异常被捕获但没有继续抛出
  • 抛出的不是运行时异常
  • 方法不是 public
  • 异步或多线程场景下事务边界变化

如果你也遇到过"明明加了事务但还是不回滚"的问题,可以先按这几个方向排查,通常能比较快缩小范围。

很多时候,事务不是没加,而是:

加了,但没有在正确的调用链路里生效。

相关推荐
七老板的blog4 分钟前
当 Spring StateMachine 遇见大模型:构建工业级 AI 写作流水线
java·人工智能·spring
云烟成雨TD24 分钟前
Spring AI 1.x 系列【46】MCP Security 模块
java·人工智能·spring
小旭95271 小时前
Spring AI Alibaba 从入门到实战:一站式掌握企业级 AI 应用开发
java·人工智能·spring
云烟成雨TD3 小时前
Spring AI 1.x 系列【50】可观测性:接入 Prometheus + Grafana
人工智能·spring·prometheus
phltxy4 小时前
MCP 从协议到 Spring AI 实战
人工智能·spring·oracle
Volunteer Technology6 小时前
SpringSecurity请求流转的本质
java·spring
云烟成雨TD8 小时前
Spring AI 1.x 系列【42】MCP 服务端 Spring Boot 启动器
java·人工智能·spring
云烟成雨TD8 小时前
Spring AI 1.x 系列【38】模型上下文协议(MCP)
java·人工智能·spring
Alson_Code8 小时前
Spring AI-1.1.0
java·人工智能·后端·spring·ai编程
小小放舟、8 小时前
@JsonCreator 注解详解——从枚举反序列化说起
spring boot·spring·spring cloud·java-ee·maven·intellij-idea·状态模式