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

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

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

前言

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

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

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

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


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

场景复现

typescript 复制代码
@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 注解不会被拦截。

用一张图理解:

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

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

解决方案

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

typescript 复制代码
@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 获取代理对象

typescript 复制代码
@Service
public class OrderService {

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

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

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


二、方法访问权限非 public

场景复现

java 复制代码
@Service
public class OrderService {

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

失效原因

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

Spring 在 AbstractFallbackTransactionAttributeSource#computeTransactionAttribute 中明确判断:

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

解决方案

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


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

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

场景 3.1:异常被 catch 吞掉

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

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

csharp 复制代码
@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

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

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

解决

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

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


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

场景复现

sql 复制代码
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):

ini 复制代码
ALTER TABLE orders ENGINE=InnoDB;

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


五、类未被 Spring 管理

场景复现

csharp 复制代码
// 没有 @Service / @Component
public class OrderService {

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

失效原因

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

解决方案

java 复制代码
@Service  // 或 @Component
public class OrderService {
    @Transactional(rollbackFor = Exception.class)
    public void insertOrder() {
        // ...
    }
}

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


六、多线程场景

场景复现

scss 复制代码
@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 等分布式事务方案
csharp 复制代码
// 方案 1:子线程独立事务
@Transactional(rollbackFor = Exception.class)
public void createOrder() {
    orderMapper.insert(order);
    // 子线程内通过独立 Service 方法开启自己的事务
    executorService.submit(() -> inventoryTxService.deduct(order.getProductId()));
}

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

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

传播行为 行为 可能导致的问题
NOT_SUPPORTED 以非事务方式执行 如果当前有事务,挂起当前事务
NEVER 如果存在事务则抛异常 直接报错
SUPPORTS 有事务就加入,没有就非事务执行 无事务时不回滚
java 复制代码
@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 代理,代理没生效,事务就失效。


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

相关推荐
不好听6131 小时前
从零搭建一个 RAG 语义搜索系统 —— DEMO的初始阶段
javascript·面试·llm
im_lanny1 小时前
Agent = Model + Harness:决定 AI 智能体上限的,往往不是模型而是“装具”
后端
阿文和她的Key1 小时前
AI新词太多?把它们串成一条线就清楚了
后端
黄敬峰1 小时前
RAG 实战:从关键词匹配到语义搜索,手把手教你用 Node.js 搭建 AI 检索引擎
面试
笨鸟飞不快1 小时前
当规则比代码跑得快:我对用 LiteFlow 编排信贷业务的一点思考
后端·设计
苏三说技术1 小时前
干掉if...else,这个规则引擎真香!
后端
xiaoshuai10241 小时前
把权限校验从手写里解放出来:RBAC 注解 + 过滤器链的设计
后端
wear工程师2 小时前
Redis 分布式锁到底靠不靠谱:从 SETNX 到 Redlock,我踩过的坑和业内的争议
redis·面试
飞天狗2 小时前
TypeScript类型系统其实是个图灵完备的语言
面试·typescript