Skip to content

Transactional 失效场景

🕒 Published at:

一文吃透 Spring @Transactional 全部失效场景,避开99%线上坑

前言

做 Java 开发没人不用 @Transactional,但线上经常遇到:明明加了事务注解,报错却不回滚

大部分人只知道同类自调用失效,其实总共有 8 大失效场景,本文一次性全部整理齐全、附带原理+错误示例,收藏一篇就够用一辈子。

一、核心底层前提(先懂原理再记场景)

Spring 事务基于 AOP 动态代理

  • 只有外部通过代理对象调用@Transactional 的方法,才会生效
  • 不走代理、不被 AOP 拦截,事务直接失效

所有失效场景,本质都是:没走到 AOP 代理拦截链


场景1:同类内部方法直接调用(this 自调用)

错误示例

java
@Service
public class OrderService {

    public void createOrder() {
        // 原生this调用,不走代理
        this.saveOrder();
    }

    @Transactional(rollbackFor = Exception.class)
    public void saveOrder() {
        // 入库 + 抛异常
        orderMapper.insert(...);
        int a = 1 / 0;
    }
}

失效原因

this原始对象,不是 Spring 代理 Bean,AOP 无法拦截,事务注解无效。

解决

  1. 本类自注入(生产最常用)
java
@Autowired
private OrderService self;

使用 self.方法() 调用,走代理。 2. AopContext 获取代理 启动类添加 @EnableAspectJAutoProxy(exposeProxy = true)

java
OrderService proxy = (OrderService) AopContext.currentProxy();
proxy.saveOrder();
  1. 业务拆分:把事务方法抽至另外 Service,彻底避免同类调用。

场景2:方法不是 public 修饰

错误示例

java
@Service
public class OrderService {

    @Transactional(rollbackFor = Exception.class)
    void saveOrder() { 
        // 默认包访问权限 / private / protected 都不行
    }
}

失效原因

Spring AOP 动态代理只拦截 public 方法,非 public 无法生成代理增强,事务直接失效。

解决

事务方法强制改为 public,不要使用 private、protected、包级私有。


场景3:异常不是「受检/运行时异常」,未配置 rollbackFor

错误认知

默认 @Transactional 只回滚 RuntimeException + Error普通 Checked 异常不回滚

错误示例

java
@Transactional
public void biz() throws Exception {
    // 抛出普通 Exception,不会回滚
    throw new Exception("业务失败");
}

失效原因

Spring 默认规则:

  • 抛出 RuntimeException / Error → 回滚
  • 抛出 Checked Exception → 只提交不回滚

正确写法

java
// 生产统一标配
@Transactional(rollbackFor = Exception.class)

场景4:try-catch 捕获了异常,没往外抛

错误示例

java
@Transactional(rollbackFor = Exception.class)
public void save() {
    try {
        userMapper.insert(...);
        int i = 1 / 0;
    } catch (Exception e) {
        // 异常被吞,Spring 感知不到
        log.error("出错", e);
    }
}

失效原因

异常被当前方法 catch 吃掉,没有抛给 AOP 事务拦截器,Spring 认为正常执行,直接提交事务。

解决

  1. 重新抛出异常(推荐)
java
catch (Exception e){
    log.error("异常",e);
    throw e;
}
  1. 手动强制回滚(不抛异常也能回滚)
java
catch (Exception e){
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}

场景5:被 final / static 修饰的事务方法

错误示例

java
@Transactional(rollbackFor = Exception.class)
public final void saveUser() { ... }

@Transactional(rollbackFor = Exception.class)
public static void saveUser() { ... }

失效原因

  • final:方法不能被重写,JDK/CGLIB 都无法代理增强
  • static:属于类不属于实例,代理对象无法覆盖 static 方法

解决

事务方法严禁加 final、static。 不要把事务写在工具类、静态工具方法中。


场景6:类没有被 Spring 托管(没交給容器)

错误示例

java
// 没有 @Service / @Component
public class UserService {
    @Transactional
    public void save() { ... }
}

// 自己 new 对象调用
UserService service = new UserService();
service.save();

失效原因

自己 new 的是普通对象,不是 Spring Bean,没有经过 AOP 代理,事务完全不生效。

解决

给当前类添加 @Service@Component必须由Spring注入Bean,禁止手动new


场景7:事务传播行为配置不当导致不回滚

典型:REQUIRES_NEW、SUPPORTS、NOT_SUPPORTED 等用错。

示例:外层无事务,内层 SUPPORTS

java
@Transactional(propagation = Propagation.SUPPORTS)
public void biz() {
    // 外层没事务,自身以非事务运行,报错不回滚
    int i = 1/0;
}

常见坑

  • Propagation.NOT_SUPPORTED:始终以非事务执行
  • Propagation.SUPPORTS:有事务就加入,没事务就非事务运行

解决

  1. 普通增删改一律使用默认 REQUIRED
  2. 查询、统计接口使用 NOT_SUPPORTED
  3. 不要随意乱用传播属性,极易造成事务失效。

场景8:多线程异步调用事务方法

错误示例

java
@Transactional(rollbackFor = Exception.class)
public void save() {
    new Thread(() -> {
        // 子线程数据库操作,不在当前事务上下文
        userMapper.insert(...);
    }).start();
}

失效原因

Spring 事务基于 ThreadLocal 绑定当前事务上下文,子线程无法继承父线程事务,各自独立事务,异常互不回滚。

解决

  1. 禁止在事务中开启子线程操作数据库
  2. 多线程业务拆分独立事务;
  3. 分布式一致性使用 Seata、MQ 最终一致性方案。

总结:8大失效场景一句话汇总

  1. 同类 this 内部调用,未走代理;
  2. 方法非 public 修饰,AOP 不拦截;
  3. 抛出受检异常,没配置 rollbackFor;
  4. 异常被 try-catch 吞掉不外抛;
  5. 方法被 final / static 修饰;
  6. 手动 new 对象,类未交给 Spring 托管;
  7. 误用 SUPPORTS / NOT_SUPPORTED 传播级别;
  8. 多线程跨线程操作,ThreadLocal 事务隔离。

收尾建议

给大家贴一份生产通用事务模板,日常业务直接照搬,避免踩坑:

java
@Transactional(rollbackFor = Exception.class)
public void businessMethod(){
    // 数据库增删改业务
}

日常开发只要守住这 8 条规则,基本能杜绝 100% 事务莫名不回滚的线上问题。