Spring 事务失效的 6 大场景,你踩过几个?

58 阅读5分钟

Spring 事务失效的 6 大场景,你踩过几个?

Spring 事务失效是我项目中踩过最多的坑之一。这篇把所有失效场景和解决方案一次性讲透,建议收藏。

前言

Spring 声明式事务用起来很简单——一个 @Transactional 注解搞定。但真到了生产环境,你会发现它在某些场景下"悄无声息"地失效了,数据不一致的 bug 极其难排查。

Spring 事务失效的根本原因只有一句话:

Spring 事务基于 AOP 代理实现,只要代理没生效,事务就会失效。

理解了这句话,下面 6 大场景都能推导出来。


一、自调用问题(最常见,踩坑率 90%)

场景复现

@Service
public class OrderService {

    public void createOrder() {
        // 直接通过 this 调用,绕过了代理对象
        this.insertOrder();
    }

    @Transactional(rollbackFor = Exception.class)
    public void insertOrder() {
        orderMapper.insert(order);
        inventoryMapper.deduct(order.getProductId());
        // 如果这里抛异常,insertOrder 的事务不会回滚!
    }
}

失效原因

Spring AOP 通过代理对象拦截方法调用来管理事务。当 createOrder() 调用 this.insertOrder() 时,this 是目标对象本身,不是代理对象,所以 @Transactional 注解不会被拦截。

用一张图理解:

调用方 → 代理对象(OrderService$Proxy) → 目标对象(OrderService)
              ↑ @Transactional 在这里拦截

createOrder() 被代理拦截 ✅
  └─ this.insertOrder() 直接在目标对象内部调用 ❌ 不经过代理

解决方案

方案 1:拆分到另一个 Service(推荐)

@Service
public class OrderService {

    @Autowired
    private OrderTxService orderTxService;

    public void createOrder() {
        orderTxService.insertOrder(); // 通过代理调用 ✅
    }
}

@Service
public class OrderTxService {
    @Transactional(rollbackFor = Exception.class)
    public void insertOrder() {
        orderMapper.insert(order);
        inventoryMapper.deduct(order.getProductId());
    }
}

方案 2:通过 AopContext 获取代理对象

@Service
public class OrderService {

    public void createOrder() {
        // 通过代理对象调用 ✅
        ((OrderService) AopContext.currentProxy()).insertOrder();
    }

    @Transactional(rollbackFor = Exception.class)
    public void insertOrder() {
        // ...
    }
}

注意:方案 2 需要在启动类上加 @EnableAspectJAutoProxy(exposeProxy = true)


二、方法访问权限非 public

场景复现

@Service
public class OrderService {

    @Transactional(rollbackFor = Exception.class)
    void insertOrder() {  // 包级私有,事务失效
        // ...
    }
}

失效原因

  • JDK 动态代理:基于接口实现,只能代理接口中定义的方法(public)。
  • CGLIB 代理:基于继承生成子类,privatefinalstatic 方法无法被重写。

Spring 在 AbstractFallbackTransactionAttributeSource#computeTransactionAttribute 中明确判断:

if (allowPublicMethodsOnly() &&
    !Modifier.isPublic(method.getModifiers())) {
    return null; // 非 public 方法,直接返回 null,不应用事务
}

解决方案

@Transactional 方法改为 public。如果不想暴露,可以拆到内部 Service 中。


三、异常被吞或异常类型不匹配

这是最隐蔽的坑,因为代码不会报错,但事务就是不回滚。

场景 3.1:异常被 catch 吞掉

@Transactional(rollbackFor = Exception.class)
public void insertOrder() {
    try {
        orderMapper.insert(order);
        inventoryMapper.deduct(order.getProductId()); // 这里抛异常
    } catch (Exception e) {
        log.error("下单失败", e);
        // 异常被吞了!事务管理器感知不到异常,不会回滚
    }
}

解决:catch 之后要重新抛出,或者手动标记回滚。

@Transactional(rollbackFor = Exception.class)
public void insertOrder() {
    try {
        orderMapper.insert(order);
        inventoryMapper.deduct(order.getProductId());
    } catch (Exception e) {
        log.error("下单失败", e);
        throw e; // 重新抛出 ✅
        // 或者:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

场景 3.2:抛出 checked 异常但未指定 rollbackFor

@Transactional // 默认只回滚 RuntimeException 和 Error
public void insertOrder() throws Exception {
    orderMapper.insert(order);
    if (stockNotEnough) {
        throw new Exception("库存不足"); // checked 异常,默认不回滚!
    }
}

原因:Spring 默认只对 RuntimeExceptionError 回滚。Exception 是 checked 异常,不在默认回滚范围。

解决

@Transactional(rollbackFor = Exception.class) // 指定回滚异常 ✅
public void insertOrder() throws Exception {
    // ...
}

最佳实践:永远加上 rollbackFor = Exception.class,不要依赖默认行为。


四、数据库存储引擎不支持事务

场景复现

CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    order_no VARCHAR(32),
    amount DECIMAL(10, 2)
) ENGINE=MyISAM;  -- MyISAM 不支持事务

失效原因

@Transactional 底层依赖数据库的事务机制。MySQL 的 MyISAM 引擎不支持事务,即使 Spring 正确开启了事务,数据库层面也无法回滚。

解决方案

使用 InnoDB 引擎(MySQL 5.5+ 默认已是 InnoDB):

ALTER TABLE orders ENGINE=InnoDB;

在一些老项目中,或者 DBA 建表时习惯用 MyISAM,需要特别注意。


五、类未被 Spring 管理

场景复现

// 没有 @Service / @Component
public class OrderService {

    @Transactional(rollbackFor = Exception.class)
    public void insertOrder() {
        // 事务失效,因为 OrderService 不是 Spring Bean
    }
}

失效原因

@Transactional 是 Spring 的注解,只有被 Spring 容器管理的 Bean 才会被 AOP 代理拦截。如果一个类没有被 Spring 管理,它就不会被代理,注解自然无效。

解决方案

@Service  // 或 @Component
public class OrderService {
    @Transactional(rollbackFor = Exception.class)
    public void insertOrder() {
        // ...
    }
}

这类问题常出现在:手动 new 对象、使用 @Bean 但方法返回的不是代理类等场景。


六、多线程场景

场景复现

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

    new Thread(() -> {
        // 新线程中的数据库操作不在事务中!
        inventoryMapper.deduct(order.getProductId());
    }).start();

    // 如果主线程回滚,子线程的操作不会回滚
}

失效原因

Spring 事务管理器通过 ThreadLocal 将事务信息(Connection、事务状态等)绑定到当前线程。新起的线程无法访问父线程的 ThreadLocal,所以无法加入同一事务。

解决方案

多线程事务是一个复杂问题,常见方案:

  1. 拆分事务:子线程内独立管理事务(@Transactional + 独立的 Service 方法)
  2. 编程式事务:使用 TransactionTemplate 手动控制
  3. 分布式事务:如果必须保证一致性,考虑 Seata 等分布式事务方案
// 方案 1:子线程独立事务
@Transactional(rollbackFor = Exception.class)
public void createOrder() {
    orderMapper.insert(order);
    // 子线程内通过独立 Service 方法开启自己的事务
    executorService.submit(() -> inventoryTxService.deduct(order.getProductId()));
}

传播行为导致事务"失效"

严格来说这不是 bug,而是配置不当导致行为与预期不符:

传播行为行为可能导致的问题
NOT_SUPPORTED以非事务方式执行如果当前有事务,挂起当前事务
NEVER如果存在事务则抛异常直接报错
SUPPORTS有事务就加入,没有就非事务执行无事务时不回滚
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void queryOrder() {
    // 以非事务方式执行,不参与外层事务
}

查询方法用 NOT_SUPPORTED 是合理的,但如果误用到写操作上就会出问题。


总结

#失效场景根本原因解决方案
1自调用this 调用绕过代理拆分 Service 或用 AopContext.currentProxy()
2方法非 public代理无法拦截非 public 方法改为 public
3异常被吞 / 类型不匹配事务管理器感知不到异常重新抛出 + rollbackFor = Exception.class
4存储引擎不支持MyISAM 无事务能力使用 InnoDB
5未被 Spring 管理不是 Bean,不会被代理@Service / @Component
6多线程ThreadLocal 无法跨线程子线程独立事务或分布式事务

记住一句话就够了:Spring 事务基于 AOP 代理,代理没生效,事务就失效。


本文基于实际项目经验整理,如有遗漏欢迎评论区补充。如果对你有帮助,点个赞👍就是最大的支持!