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

📅 2026/7/18 5:50:01
Spring @Transactional 事务失效:原理层面深度解析
核心前提Spring 事务的本质一切问题的根源在于——Spring 的事务管理本质上是基于 AOP 代理实现的而不是像 EJB 那样由容器原生拦截。客户端调用 → 代理对象(拦截) → 真实对象(目标方法) → 返回 ↑ 事务开启/提交/回滚 在这里完成理解这一点所有失效场景都能从这个模型中推导出来。一、自调用问题最经典原理ServicepublicclassOrderService{publicvoidcreateOrder(){// ① 外部方法没有 Transactionalthis.processPayment();// ② 内部调用直接通过 this 引用}TransactionalpublicvoidprocessPayment(){// 期望事务保护但实际上没有事务}}发生了什么调用链路对比 外部调用 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方法ServicepublicclassUserService{TransactionalprotectedvoidinternalTransfer(){// protected非 public// CGLIB 子类可以覆盖 protected 方法但...}TransactionalprivatevoiddoSomething(){// private// CGLIB 根本无法覆盖 private 方法拦截器无法织入}TransactionalfinalvoidfinalizeTransfer(){// final// CGLIB 子类无法 override final 方法}}Spring AOP 的源码佐证// AbstractFallbackTransactionAttributeSource.javaprotectedTransactionAttributecomputeTransactionAttribute(Methodmethod,Class?targetClass){// 非 public 方法直接返回 null不处理事务if(allowPublicMethodsOnly()!Modifier.isPublic(method.getModifiers())){returnnull;}// ...}根因Spring AOP无论是 JDK Proxy 还是 CGLIB在设计上就无法拦截非 public 方法。这是代理机制的根本限制不是 Spring 的 bug而是有意为之的架构决策——非 public 方法属于类的内部实现细节不应该被外部增强。三、异常类型不匹配原理Spring 事务的回滚机制// TransactionInterceptor 的核心逻辑简化try{resultinvocation.proceed();}catch(Throwableex){// 关键判断这个异常需要回滚吗if(txInfo.transactionAttribute!nulltxInfo.transactionAttribute.rollbackOn(ex)){// ← 这里// 回滚tx.getTransactionManager().rollback(txInfo.getTransactionStatus());}else{// 不回滚提交事务tx.getTransactionManager().commit(txInfo.getTransactionStatus());}throwex;}默认规则rollbackOn 的默认实现 (RuleBasedTransactionAttribute) RuntimeException (unchecked) → 回滚 ✅ Error → 回滚 ✅ Checked Exception → 不回滚 ❌ ← 这是默认行为// 默认的回滚规则源码OverridepublicbooleanrollbackOn(Throwableex){if(logger.isTraceEnabled()){logger.trace(Invoking rollbackOn with exception: ex);}// 内置规则RuntimeException 和 Error 回滚if(exinstanceofRuntimeException||exinstanceofError){returntrue;}// 其他异常默认不回滚returnfalse;}所以当你的代码是ServicepublicclassFileService{Transactionalpublicvoidupload()throwsIOException{// Checked Exception// 写入数据库...thrownewIOException(磁盘满了);// ❌ 事务提交了数据不一致}}修复方式Transactional(rollbackFor Exception.class)四、传播行为导致新事务原理Transactional(propagationPropagation.REQUIRES_NEW)publicvoidprocessPayment(){// 这个方法会在一个全新的事务中执行}事务传播的 7 种行为核心决策树当前是否存在事务 ├── 否 │ ├── REQUIRED → 创建新事务 │ ├── REQUIRES_NEW → 创建新事务 │ └── NESTED → 创建新事务 └── 是 ├── REQUIRED → 加入当前事务 ├── REQUIRES_NEW → 挂起当前事务创建新事务 ← 关键 ├── NESTED → 创建保存点嵌套事务 ├── NOT_SUPPORTED → 挂起当前事务 └── NEVER → 抛异常典型的失效场景ServicepublicclassOrderService{TransactionalpublicvoidcreateOrder(){saveOrder();// 在外层事务中paymentService.pay();// ← 注意这里}}ServicepublicclassPaymentService{Transactional(propagationPropagation.REQUIRES_NEW)publicvoidpay(){// 独立的新事务// 如果这里抛异常回滚外层事务不受影响除非异常向上抛出// 如果这里正常提交即使外层事务回滚pay 的数据也不回滚}}时间线 外层事务: ───[开启]──────────────────────────────[回滚]─── 内层事务: ───[开启]──[提交]─── ↑ pay 的数据已经持久化不会被外层回滚这不是 bug而是REQUIRES_NEW的语义。但如果不理解传播机制就会觉得事务失效了。五、数据库引擎不支持事务原理Spring 的事务管理器最终会调用Connection的事务方法PlatformTransactionManager → DataSourceTransactionManager → Connection.setAutoCommit(false) ← 开启事务 → Connection.commit() ← 提交 → Connection.rollback() ← 回滚但这些调用是否真正生效取决于数据库存储引擎-- MySQL 中MyISAM 引擎完全忽略事务语句CREATETABLEorders(...)ENGINEMyISAM;-- ❌ 事务完全无效-- InnoDB 才支持真正的 ACID 事务CREATETABLEorders(...)ENGINEInnoDB;-- ✅MySQL 的 JDBC 驱动即使对 MyISAM 表调用commit()/rollback()也会静默成功不会报错。Spring 层面毫无感知。六、多数据源/多事务管理器原理当存在多个数据源时Spring 不知道该用哪个事务管理器ConfigurationpublicclassDataSourceConfig{Bean(ds1TransactionManager)publicPlatformTransactionManagertm1(Qualifier(ds1)DataSourceds){returnnewDataSourceTransactionManager(ds);}Bean(ds2TransactionManager)publicPlatformTransactionManagertm2(Qualifier(ds2)DataSourceds){returnnewDataSourceTransactionManager(ds);}}ServicepublicclassTransferService{Transactional// ❓ 用哪个 TransactionManagerpublicvoidtransfer(){// ds1 操作...// ds2 操作...// 只有一个数据源的事务被管理}}Transactional的查找规则// AnnotationTransactionAttributeSource 的解析顺序// 1. 方法上的 Transactional 注解的 value/transactionManager 属性// 2. 类上的 Transactional 注解的 value/transactionManager 属性// 3. 全局默认 → TransactionManagementConfigurer 指定// 4. 按类型查找唯一匹配的 PlatformTransactionManager// 5. 如果找到多个 → BeanCreationException根因Spring 事务是绑定到具体的Connection/DataSource的。Transactional只能控制一个事务管理器跨数据源的一致性需要 JTA 或其他分布式事务方案。七、异步方法中的事务原理ServicepublicclassNotificationService{AsyncTransactionalpublicvoidsendNotification(){// ❌ 事务在另一个线程中与调用方的事务完全隔离}}线程模型主线程事务T1: 异步线程: 调用 sendNotification() → 代理拦截 Async → 提交一个任务到线程池 ──→ 线程池线程获取任务 → 立即返回 → 代理拦截 Transactional → 开启新事务 T2 → 执行方法体 → 提交/回滚 T2 如果 T1 回滚T2 已经提交了不受影响 如果 T2 回滚T1 可能已经提交了不受影响Async的代理在Transactional的代理外层异步执行器把方法体丢到了另一个线程事务的上下文ThreadLocal中的TransactionSynchronizationManager无法跨线程传递。八、异常被吞掉原理回看事务拦截器的核心逻辑——它靠捕获异常来决定回滚try{resultinvocation.proceed();// 执行目标方法returnresult;}catch(Throwableex){// 只有抛到这里才有机会判断是否回滚completeTransactionAfterThrowing(txInfo,ex);throwex;}// 方法正常返回 → 提交事务如果异常在方法内部被吞掉TransactionalpublicvoidcreateOrder(){try{saveToDatabase(order);thrownewRuntimeException(库存不足);}catch(Exceptione){log.error(出错了,e);// 异常被吞掉没有重新抛出}// 代码正常走到这里 → 事务提交 → 脏数据写入}事务拦截器看到的是目标方法正常返回了于是执行commit()。它完全不知道内部发生了什么。总结一张完整的失效地图一句话总结Spring 事务的全部能力都建立在代理能拦截到方法调用这个前提上。一旦代理被绕过自调用、代理无法生效非 public/final、拦截器拿到的信息与预期不符异常类型、传播行为、或者底层存储不支持引擎问题事务就会失效。这不是 bug而是架构设计的必然结果。