Spring @Transactional 事务失效:原理层面深度解析

核心前提:Spring 事务的本质

一切问题的根源在于------Spring 的事务管理本质上是基于 AOP 代理实现的,而不是像 EJB 那样由容器原生拦截。

复制代码
客户端调用 → 代理对象(拦截) → 真实对象(目标方法) → 返回
                ↑
        事务开启/提交/回滚 在这里完成

理解这一点,所有失效场景都能从这个模型中推导出来。


一、自调用问题(最经典)

原理

java 复制代码
@Service
public class OrderService {
    
    public void createOrder() {
        // ① 外部方法,没有 @Transactional
        this.processPayment();  // ② 内部调用,直接通过 this 引用
    }
    
    @Transactional
    public void processPayment() {
        // 期望事务保护,但实际上没有事务
    }
}

发生了什么:

复制代码
调用链路对比:

外部调用:
  client → proxy.processPayment() → [事务拦截器] → target.processPayment()
  ✅ 事务生效

内部自调用:
  target.createOrder() → this.processPayment()  ← this 是目标对象本身
                         ↑ 
                    绕过了代理,拦截器根本没机会执行
  ❌ 事务失效

根因: this 引用指向的是原始对象(target),而不是代理对象(proxy)。方法调用发生在同一个对象内部,不经过代理,AOP 拦截器就无法织入事务逻辑

本质

这是一个对象引用 的问题,不是注解的问题。Java 中 this.method() 是一次普通的 Java 方法调用,没有任何机会插入额外逻辑。


二、非 public 方法

原理

Spring 默认使用两种 AOP 实现:

实现方式 代理机制 对方法的要求
JDK 动态代理 基于接口,Proxy.newProxyInstance() 只能代理接口中的方法
CGLIB 基于继承,生成子类覆盖方法 不能代理 private/final 方法
java 复制代码
@Service
public class UserService {
    
    @Transactional
    protected void internalTransfer() {  // protected,非 public
        // CGLIB 子类可以覆盖 protected 方法,但...
    }
    
    @Transactional
    private void doSomething() {  // private
        // CGLIB 根本无法覆盖 private 方法,拦截器无法织入
    }
    
    @Transactional
    final void finalizeTransfer() {  // final
        // CGLIB 子类无法 override final 方法
    }
}

Spring AOP 的源码佐证:

java 复制代码
// AbstractFallbackTransactionAttributeSource.java
protected TransactionAttribute computeTransactionAttribute(
        Method method, Class<?> targetClass) {
    
    // 非 public 方法直接返回 null,不处理事务
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
        return null;
    }
    // ...
}

根因: Spring AOP(无论是 JDK Proxy 还是 CGLIB)在设计上就无法拦截非 public 方法。这是代理机制的根本限制,不是 Spring 的 bug,而是有意为之的架构决策------非 public 方法属于类的内部实现细节,不应该被外部增强。


三、异常类型不匹配

原理

Spring 事务的回滚机制:

java 复制代码
// TransactionInterceptor 的核心逻辑(简化)
try {
    result = invocation.proceed();
} catch (Throwable ex) {
    // 关键判断:这个异常需要回滚吗?
    if (txInfo.transactionAttribute != null &&
        txInfo.transactionAttribute.rollbackOn(ex)) {  // ← 这里
        // 回滚
        tx.getTransactionManager().rollback(txInfo.getTransactionStatus());
    } else {
        // 不回滚!提交事务
        tx.getTransactionManager().commit(txInfo.getTransactionStatus());
    }
    throw ex;
}

默认规则:

复制代码
rollbackOn 的默认实现 (RuleBasedTransactionAttribute):

RuntimeException (unchecked) → 回滚 ✅
Error                        → 回滚 ✅
Checked Exception            → 不回滚 ❌ ← 这是默认行为!
java 复制代码
// 默认的回滚规则源码
@Override
public boolean rollbackOn(Throwable ex) {
    if (logger.isTraceEnabled()) {
        logger.trace("Invoking rollbackOn with exception: " + ex);
    }
    // 内置规则:RuntimeException 和 Error 回滚
    if (ex instanceof RuntimeException || ex instanceof Error) {
        return true;
    }
    // 其他异常默认不回滚
    return false;
}

所以当你的代码是:

java 复制代码
@Service
public class FileService {
    
    @Transactional
    public void upload() throws IOException {  // Checked Exception
        // 写入数据库...
        throw new IOException("磁盘满了");  // ❌ 事务提交了!数据不一致!
    }
}

修复方式: @Transactional(rollbackFor = Exception.class)


四、传播行为导致新事务

原理

java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processPayment() {
    // 这个方法会在一个全新的事务中执行
}

事务传播的 7 种行为,核心决策树:

复制代码
当前是否存在事务?
├── 否
│   ├── REQUIRED      → 创建新事务
│   ├── REQUIRES_NEW   → 创建新事务
│   └── NESTED         → 创建新事务
└── 是
    ├── REQUIRED       → 加入当前事务
    ├── REQUIRES_NEW   → 挂起当前事务,创建新事务 ← 关键!
    ├── NESTED         → 创建保存点(嵌套事务)
    ├── NOT_SUPPORTED  → 挂起当前事务
    └── NEVER          → 抛异常

典型的"失效"场景:

java 复制代码
@Service
public class OrderService {

    @Transactional
    public void createOrder() {
        saveOrder();           // 在外层事务中
        paymentService.pay();  // ← 注意这里
    }
}

@Service
public class PaymentService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void pay() {
        // 独立的新事务!
        // 如果这里抛异常回滚,外层事务不受影响(除非异常向上抛出)
        // 如果这里正常提交,即使外层事务回滚,pay 的数据也不回滚!
    }
}
复制代码
时间线:
外层事务:  ───[开启]──────────────────────────────[回滚]───
内层事务:            ───[开启]──[提交]───
                              ↑
                   pay 的数据已经持久化,不会被外层回滚

这不是 bug,而是 REQUIRES_NEW 的语义。但如果不理解传播机制,就会觉得"事务失效了"。


五、数据库引擎不支持事务

原理

Spring 的事务管理器最终会调用 Connection 的事务方法:

复制代码
PlatformTransactionManager
  → DataSourceTransactionManager
    → Connection.setAutoCommit(false)   ← 开启事务
    → Connection.commit()               ← 提交
    → Connection.rollback()             ← 回滚

但这些调用是否真正生效,取决于数据库存储引擎:

sql 复制代码
-- MySQL 中,MyISAM 引擎完全忽略事务语句
CREATE TABLE orders (...) ENGINE=MyISAM;  -- ❌ 事务完全无效

-- InnoDB 才支持真正的 ACID 事务
CREATE TABLE orders (...) ENGINE=InnoDB;  -- ✅

MySQL 的 JDBC 驱动即使对 MyISAM 表调用 commit()/rollback(),也会静默成功,不会报错。Spring 层面毫无感知。


六、多数据源/多事务管理器

原理

当存在多个数据源时,Spring 不知道该用哪个事务管理器:

java 复制代码
@Configuration
public class DataSourceConfig {
    
    @Bean("ds1TransactionManager")
    public PlatformTransactionManager tm1(@Qualifier("ds1") DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
    
    @Bean("ds2TransactionManager")
    public PlatformTransactionManager tm2(@Qualifier("ds2") DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
}

@Service
public class TransferService {
    
    @Transactional  // ❓ 用哪个 TransactionManager?
    public void transfer() {
        // ds1 操作...
        // ds2 操作...
        // 只有一个数据源的事务被管理!
    }
}

@Transactional 的查找规则:

java 复制代码
// AnnotationTransactionAttributeSource 的解析顺序:
// 1. 方法上的 @Transactional 注解的 value/transactionManager 属性
// 2. 类上的 @Transactional 注解的 value/transactionManager 属性
// 3. 全局默认 → TransactionManagementConfigurer 指定
// 4. 按类型查找唯一匹配的 PlatformTransactionManager
// 5. 如果找到多个 → BeanCreationException

根因: Spring 事务是绑定到具体的 Connection/DataSource 的。@Transactional 只能控制一个事务管理器,跨数据源的一致性需要 JTA 或其他分布式事务方案。


七、异步方法中的事务

原理

java 复制代码
@Service
public class NotificationService {
    
    @Async
    @Transactional
    public void sendNotification() {
        // ❌ 事务在另一个线程中,与调用方的事务完全隔离
    }
}

线程模型:

复制代码
主线程(事务T1):                    异步线程:
  调用 sendNotification()
  → 代理拦截 @Async
  → 提交一个任务到线程池     ──→   线程池线程获取任务
  → 立即返回                       → 代理拦截 @Transactional
                                   → 开启新事务 T2
                                   → 执行方法体
                                   → 提交/回滚 T2

如果 T1 回滚,T2 已经提交了,不受影响
如果 T2 回滚,T1 可能已经提交了,不受影响

@Async 的代理在 @Transactional 的代理外层 ,异步执行器把方法体丢到了另一个线程,事务的上下文(ThreadLocal 中的 TransactionSynchronizationManager)无法跨线程传递。


八、异常被吞掉

原理

回看事务拦截器的核心逻辑------它靠捕获异常来决定回滚

java 复制代码
try {
    result = invocation.proceed();  // 执行目标方法
    return result;
} catch (Throwable ex) {
    // 只有抛到这里,才有机会判断是否回滚
    completeTransactionAfterThrowing(txInfo, ex);
    throw ex;
}
// 方法正常返回 → 提交事务

如果异常在方法内部被吞掉:

java 复制代码
@Transactional
public void createOrder() {
    try {
        saveToDatabase(order);
        throw new RuntimeException("库存不足");
    } catch (Exception e) {
        log.error("出错了", e);  // 异常被吞掉,没有重新抛出
    }
    // 代码正常走到这里 → 事务提交 → 脏数据写入
}

事务拦截器看到的是: 目标方法正常返回了,于是执行 commit()。它完全不知道内部发生了什么。


总结:一张完整的失效地图

一句话总结: Spring 事务的全部能力都建立在"代理能拦截到方法调用"这个前提上。一旦代理被绕过(自调用)、代理无法生效(非 public/final)、拦截器拿到的信息与预期不符(异常类型、传播行为)、或者底层存储不支持(引擎问题),事务就会"失效"。这不是 bug,而是架构设计的必然结果。

相关推荐
没钥匙的锁12 小时前
16-Java反射机制:Spring IOC背后的核心技术
java·开发语言·spring
geinvse_seg2 小时前
飞算JavaAI:把企业采购协同需求拆成一套可运行的慧采平台
java·ai·飞算
春卷同学3 小时前
HarmonyOS掌上记账APP开发实践第23篇:弹性动效与手势 — EdgeEffect.Spring 与手势系统实战
spring·华为·harmonyos
烛影摇红透纱窗3 小时前
ForkJoinPool 与工作窃取
java
浩瀚地学4 小时前
【面试算法笔记】0302-哈希表-哈希表实现
java·经验分享·笔记·算法·面试
AC赳赳老秦4 小时前
时间开销自动统计:OpenClaw 记录工作任务时长、分析时间分配、给出优化建议
java·大数据·开发语言·python·自动化·deepseek·openclaw
名侦探7224 小时前
构建 Agent 的五大难点与解决方案
java·大数据·人工智能
Demons_kirit4 小时前
图库原理(以阿里云OSS为案例)
java·阿里云·云计算
SimonKing4 小时前
那个号称"对标 Spring"的国产框架 Solon,到底行不行?
java·后端·程序员