核心前提: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,而是架构设计的必然结果。