Skip to content

@Transactional 失效场景大全(含自调用、传播机制)

问题

在 Spring 项目中,为什么有时候 @Transactional 加了注解,事务却就是不回滚?自调用场景下事务为什么失效?不同的传播机制又会引发哪些坑?


分析

@Transactional 是 Spring 声明式事务的入口,但它的生效依赖于 AOP 代理机制。一旦代理链路被破坏——无论是因为绕过代理对象、异常类型不匹配,还是底层引擎不支持——事务就会静默失效。下面逐一拆解 7 个最常见场景,每个都给出原因和代码印证。

场景一:自调用(Self-Invocation)—— 最经典的生产事故来源

失效现象

同一个类中,非事务方法调用 @Transactional 方法,事务不会生效。

java
@Service
public class OrderService {

    public void createOrder() {
        // 自调用 - this 引用,不是代理对象
        this.saveOrder();  // @Transactional 不生效!
    }

    @Transactional
    public void saveOrder() {
        // insert into order...
        // insert into order_item...
    }
}

调用链路全程拆解

理解这个坑,必须知道 Spring AOP 代理在方法调用时的完整时序:

[调用方] → proxy.saveOrder() → TransactionInterceptor.invoke()
    → 检查 @Transactional 注解 → 获取 TransactionAttribute
    → TransactionManager.getTransaction() → 创建/获取数据库连接,设置 autoCommit=false
    → 反射调用目标对象 OrderService.saveOrder()  ← 这才是实际代码
    → 执行完毕 → 无异常 → TransactionManager.commit()
    → 有异常 → TransactionManager.rollback()

this.saveOrder() 中的 this 指向的是原始目标对象,不是代理对象。调用链变成了:

[调用方] → proxy.createOrder() → TransactionInterceptor 发现 createOrder 没有 @Transactional
    → 直接 invoke 目标对象.createOrder()
    → 目标对象内 this.saveOrder()  ← this 是原始对象,不是 proxy
    → 直接调用原始对象.saveOrder()
    → 跳过 TransactionInterceptor!
    → 无事务增强,每条 SQL 自动提交

为什么 CGLIB 也救不了

很多人以为换成 CGLIB 代理就能解决。CGLIB 确实可以代理非接口方法,但 this 引用在 Java 语言层面就是指向当前对象实例,Spring 没有能力在运行时替换掉 this 的指向。无论 JDK 动态代理还是 CGLIB,this 永远指向目标对象,而不是代理对象。

三种解决方案的落地对比

方案代码示例实际场景推荐坑点
抽离独立 ServicepaymentService.saveOrder(order)最推荐:职责清晰,单元测试也方便如果 Service 太多会膨胀,但比事务问题好
注入自身代理@Autowired OrderService self; self.saveOrder(order)次选:不想改类结构时用循环依赖风险(Spring 通过三级缓存能解,但 ObjectProvider 延迟注入更安全)
AopContext.currentProxy()((OrderService) AopContext.currentProxy()).saveOrder()不推荐:代码可读性差,必须 @EnableAspectJAutoProxy(exposeProxy=true)显式转型失败会在运行时才暴露,且 exposeProxy 有性能开销

生产踩坑记录:某电商项目在订单创建流程中,createOrder 调用了 @TransactionalupdateInventory,因为自调用导致库存扣减不在事务内。压测时出现并发超卖,MySQL 实际扣减了 -3 件库存。排查 3 小时才发现是 this.updateInventory() 的问题。

场景二:非 public 方法

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

源码级验证AbstractFallbackTransactionAttributeSource.computeTransactionAttribute() 方法中,有一行:

java
if (method.getModifiers() != Modifier.PUBLIC) {
    return null;
}

JDK 动态代理只代理接口方法(一定是 public),CGLIB 虽然可以代理非 public 方法,但 Spring 在这里显式跳过了。所以 protectedpackage-privateprivate 一律不生效。

面试追问:如果方法加了 @Transactional 且是 public,但类没有实现接口,Spring 默认怎么处理?—— 如果目标类没有实现接口,Spring 自动降级为 CGLIB 代理,@Transactional 仍然生效。

场景三:异常类型不对 —— 默认不回滚检查异常

java
@Transactional
public void saveOrder() {
    // ...
    throw new SQLException("插入失败");  // 不回滚!
}

原因TransactionAspectSupportrollbackOn() 方法只对 RuntimeExceptionError 返回 trueSQLException 是检查异常,继承自 Exception,不满足条件,所以事务管理器执行 commit()

真实数据:某金融系统对账模块,用 @Transactional 包裹对账逻辑,FileNotFoundException 被抛出后事务没有回滚,对账记录部分写入。修复前线上平均每月 2 次数据不一致。

修复方案

java
@Transactional(rollbackFor = {SQLException.class, FileNotFoundException.class, Exception.class})
public void saveOrder() {
    // ...
    throw new SQLException("插入失败");  // 现在可以回滚了
}

冷知识@Transactional(noRollbackFor = ...) 也可以指定不回滚的异常。如果业务层做了异常分类,可以在最外层统一 rollbackFor = Exception.class,内层用 noRollbackFor 放行业务异常。

场景四:异常被 catch 吞掉 —— 事务管理器以为你成功了

java
@Transactional
public void saveOrder() {
    try {
        // insert...
        int i = 1 / 0;  // 抛出 ArithmeticException
    } catch (Exception e) {
        log.error("插入失败", e);
        // 没有重新抛出!
    }
}

执行流程

TransactionInterceptor.invoke(methodInvocation):
    → TransactionManager.getTransaction()
    → 反射调用目标方法 saveOrder()
        → 进入 try-catch → catch 吞掉异常 → 方法正常返回
    → 回到 TransactionInterceptor:
    → commitTransactionAfterReturning()  ← 提交了,因为没看到异常
    → 连接池归还连接 ← 事务已经提交

真实场景:某网关服务,在 @Transactional 方法中调用了外部 RPC 接口,RPC 超时抛出异常被 catch 后记录日志,没有重新抛出。结果主表数据已回滚但业务逻辑没感知到回滚,后续补偿逻辑反而把正确数据覆盖了。

解决:要么不 catch 异常,要么 catch 后重新抛出:

java
@Transactional
public void saveOrder() {
    try {
        // insert...
    } catch (Exception e) {
        log.error("插入失败", e);
        // 方案 A:重新抛出
        throw e;
        // 方案 B:手动标记回滚
        // TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

方案 B 在不希望抛出异常(比如上层有统一处理逻辑)时有用,但要注意 setRollbackOnly() 不会阻止后续代码执行,需要手动 return

场景五:传播机制导致事务边界失控

java
@Transactional
public void outer() {
    orderDao.insert(order);
    inner();  // REQUIRES_NEW 开启新事务
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() {
    itemDao.insert(item);
    throw new RuntimeException("内部失败");
}

执行时序

outer() 的事务开始 → 获取连接 conn1
    → orderDao.insert(order)  // 写入 conn1
    → inner() 被调用:
        → REQUIRES_NEW: 暂停 conn1 上的事务
        → 获取新连接 conn2
        → 开启 conn2 上的新事务
        → itemDao.insert(item)  // 写入 conn2
        → throw RuntimeException
        → conn2 事务回滚 ✅
        → 恢复 conn1
    → outer() 感知到异常,conn1 事务也回滚 ✅

这里两个事务都回滚了,看起来没问题。但问题出在混合传播机制 + 异常捕获的场景:

java
@Transactional
public void outer() {
    orderDao.insert(order);
    try {
        inner();  // REQUIRES_NEW
    } catch (Exception e) {
        log.warn("内部失败,但外部继续", e);
    }
    orderDao.updateStatus(order.getId(), "PARTIAL");  // 继续执行
}

此时 inner() 的事务已经回滚,但 outer() 因为 catch 了异常,继续执行并提交。结果是:order 表插入了记录,item 表没插入,业务数据不一致。

连接池耗尽风险(高并发重点)

REQUIRES_NEW 在事务执行期间,会持有外部事务的连接(conn1),同时获取新连接(conn2)。

假设连接池大小 = 10
线程1: 持有 conn1, 使用 conn2 → 剩余 8 个连接
线程2: 持有 conn3, 使用 conn4 → 剩余 6 个连接
...
线程5: 持有 conn9, 使用 conn10 → 剩余 0 个连接
线程6: 需要一个新连接 → 阻塞等待! ← 死锁?

这不是死锁,但等价于死锁——所有线程都持有旧连接等待新连接,没有线程释放连接。最终导致连接池不可用,服务假死。

最佳实践REQUIRES_NEW 慎用。如果必须用,设置连接池最小空闲连接数 >= 2 × 预期并发数,并在 @Transactional 方法中避免长时间操作。

场景六:数据库引擎不支持事务

java
// application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/myapp?useSSL=false

如果某张表是 ENGINE=MyISAM,那无论怎么加 @Transactional,这条 SQL 都不会参与事务——MyISAM 不支持事务,每条 SQL 自动提交。

验证方法

sql
SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'myapp' AND TABLE_NAME = 'orders';

生产坑:某项目把日志表设计为 MyISAM 以提高写入速度,后来业务需求要求日志写入和主表更新在一个事务内。结果 @Transactional 保证不了 MyISAM 表的回滚,线上出现「主表已回滚、日志表已写入」的数据不一致。

建表统一规范:所有业务表 ENGINE=InnoDB,日志/归档表单独处理,且不要在 @Transactional 方法中同时写入 InnoDB 和 MyISAM 表。

场景七:多数据源事务未配置

java
@Transactional
public void save() {
    primaryDao.insert(data);    // 主数据源,事务生效
    secondaryDao.insert(data);  // 次数据源,不在事务管理范围内
}

原因@Transactional 默认绑定到 PrimaryDataSourceTransactionManagersecondaryDaoDataSource 不同,它不会参与同一个事务。

可行的方案对比

方案原理适用场景缺点
ChainedTransactionManager顺序提交/回滚多个 TM少量数据源,低并发不是真正的两阶段提交,第二个 TM 提交失败时第一个已提交
JTA + AtomikosXA 两阶段提交高一致性要求配置复杂,性能开销大,XA 协议受数据库驱动限制
最终一致性 + 补偿本地事务 + 消息队列高可用、可接受短暂不一致需要额外实现补偿逻辑
Seata AT自动补偿 SQL 逆操作微服务分布式事务引入 Seata 依赖,对 SQL 有约束

不使用 ChainedTransactionManager 的替代方案:如果两个数据源之间可以容忍短暂不一致,用本地消息表 + 定时补偿是最可靠的。


代码示例:自调用 + 异常类型 + 传播机制综合排查

java
@Service
@Slf4j
public class PaymentService {

    @Autowired
    private PaymentService self;  // 注入自身代理

    @Transactional
    public void processPayment(Order order) {
        try {
            // 扣减余额
            accountService.deduct(order.getUserId(), order.getAmount());
            // 创建订单 - 通过代理调用,自调用问题解决
            self.createOrderWithTx(order);
        } catch (Exception e) {
            log.error("支付失败", e);
            // 异常已抛出,事务会回滚
            throw e;
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW,
                   rollbackFor = Exception.class)
    public void createOrderWithTx(Order order) {
        orderDao.insert(order);
        // 模拟库存扣减失败
        throw new RuntimeException("库存不足");
    }
}

关键点:

  • 通过 @Autowired private PaymentService self 避免自调用
  • rollbackFor = Exception.class 确保所有异常都回滚
  • REQUIRES_NEWcreateOrderWithTx 开启独立事务,不会因为内部异常导致外部事务意外回滚(但需要仔细设计业务语义)

总结

场景根因识别手段解决
自调用this 绕过代理断点看调用栈是否经过 CglibAopProxy / JdkDynamicAopProxy注入自身代理 / 抽离独立 Service
非 public 方法Spring 方法过滤器源码 AbstractFallbackTransactionAttributeSourceModifier.isPublic() 校验改为 public
异常类型不对默认只回滚 RuntimeException确认异常继承链是否为 RuntimeException指定 rollbackFor
异常被吞事务管理器未感知异常查看 TransactionInterceptorinvokeWithinTransaction 是否 catch 了不吞异常,或手动 setRollbackOnly()
传播机制事务边界与预期不符查看 TransactionSynchronizationManager 中挂起的事务资源理解 REQUIRED/REQUIRES_NEW/NESTED 语义
引擎不支持MyISAM执行 SHOW CREATE TABLE 确认引擎建表用 InnoDB
多数据源多 DataSource 无统一事务管理查看 TransactionManager 绑定的 DataSource用 ChainedTransactionManager 或最终一致性

@Transactional 失效不是 bug,而是对 Spring 代理机制理解不够深。掌握这些场景,线上排查事务问题时,第一步就能定位到方向。

面试高频追问速查

  1. 同一个类中,@Transactional 方法 A 调用 @Transactional 方法 B,两个事务都会生效吗? → 不会。this.B() 依然是自调用,B 的事务不生效。

  2. @Transactional 什么时候走 JDK 动态代理,什么时候走 CGLIB? → 类实现了接口 → JDK 动态代理;没有实现接口或显式 proxyTargetClass=true → CGLIB。

  3. REQUIRES_NEW 和 NESTED 有什么区别? → REQUIRES_NEW 完全挂起外部事务,独立开启新连接。NESTED 使用 Savepoint,回滚到保存点而非完全独立。NESTED 只有 DataSourceTransactionManager 支持。

  4. @Transactional 和 @Transactional(propagation=REQUIRED) 有什么区别? → 没有区别。REQUIRED 是默认值。

  5. @Transactional 方法中调用了 RPC 接口,RPC 超时了,事务会怎样? → 如果 RPC 超时抛出 RuntimeException,且没有 catch 吞掉,事务会回滚。但 RPC 调用已经发出,对方可能已经执行了操作——这就是典型的「本地事务 + 远程调用」不一致问题。

参考资料:Spring 源码 — TransactionInterceptor;Spring 官方文档 — Transaction Management

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。