Spring 事务失效

一、底层原理

Spring 事务是 AOP 实现的,通过 TransactionInterceptor 包装目标方法,生成代理对象(JDK 动态代理 / CGLIB),方法调用时先经过拦截器,根据事务回滚默认规则,决定是否开启事务、提交或回滚。

方法调用时先经过拦截器,拦截器就是 Spring 为 @Transactional 创建的 TransactionInterceptor。它被织入代理对象的拦截器链里,JDK 动态代理通过 InvocationHandler,CGLIB 通过 MethodInterceptor,调用代理对象方法时会先执行 TransactionInterceptor,然后再执行目标方法。

TransactionInterceptor.invoke() 本身只负责事务逻辑,它位于代理对象的拦截器链中。方法调用时先进入拦截器链,每个拦截器依次调用 invoke(),TransactionInterceptor 内部直接调用 method.invoke(target, args) 是为了执行链中下一个环节或最终目标方法。拦截器链的循环和调用顺序是在代理对象生成阶段就已经确定的。

TransactionInterceptor 核心逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
invoke() {
// 1️⃣ 根据事务传播行为检查当前线程是否已有事务
// 决定开启新事务还是加入已有事务
TransactionStatus status = transactionManager.getTransaction(definition);

try {
// 2️⃣ 调用目标方法
Object result = method.invoke(target, args);

// 3️⃣ 方法执行成功 → 提交事务
transactionManager.commit(status);
return result;
} catch (Throwable ex) {
// 4️⃣ 方法异常 → 判断是否回滚(RuntimeException 默认回滚)
transactionManager.rollback(status);
throw ex;
}
}

调用链路图

1
2
3
4
5
6
7
8
9
10
11
12
调用代理对象.method()

▼ 拦截器链循环

├─> 事务拦截器 TransactionInterceptor.invoke()
│ ├─> 开启/加入事务
│ ├─> 调用 method.invoke(target, args)
│ └─> 提交/回滚事务

├─> 其他拦截器(Logging, Security...)

└─> 最终调用目标方法

二、几种常见失效场景

1. 未捕获异常

如果一个事务方法中发生了未捕获的异常,并且异常未被处理或传播到事务边界之外,那么事务会失效,所有的数据库操作会回滚。

2. 非受检异常

默认情况下,Spring 对非受检异常(RuntimeException 或其子类)进行回滚处理,这意味着当事务方法中抛出这些异常时,事务会回滚。

3. 事务传播属性设置不当

如果在多个事务之间存在事务嵌套,且事务传播属性配置不正确,可能导致事务失效。特别是在方法内部调用有 @Transactional 注解的方法时要特别注意。

4. 多数据源的事务管理

如果在使用多数据源时,事务管理没有正确配置或者存在多个 @Transactional 注解时,可能会导致事务失效。

5. 跨方法调用事务问题

如果一个事务方法内部调用另一个方法,而这个被调用的方法没有 @Transactional 注解,这种情况下外层事务可能会失效。

6. 事务在非公开方法中失效

如果 @Transactional 注解标注在私有方法上或者非 public 方法上,事务也会失效。

7. 使用 this 调用是否生效

因为 Spring 事务是通过代理对象来控制的,只有通过代理对象的方法调用才会应用事务管理的相关规则。当使用 this 直接调用时,是绕过了 Spring 的代理机制,因此不会应用事务设置。