Skip to content
Go back

Spring事务——传播行为与@Transactional失效的6种场景

Spring 事务:@Transactional 失效的 6 种场景

一句话结论(30s)

@Transactional 的本质是基于 AOP 代理——TransactionInterceptor 在目标方法前后通过 PlatformTransactionManager 管理事务,不是魔法。所有失效场景的根因几乎都是「调用未经过 AOP 代理」,因为同类内 this.method() 直调原始对象、非 public / final 方法无法被 CGLIB 拦截、异常被 catch 吞掉等,都会让事务拦截器根本没机会执行;其中 REQUIRES_NEW 挂起当前事务用新 Connection 开独立事务,NESTED 不挂起而是用 Savepoint 回滚到保存点。

核心原理(2min)

主流程:TransactionInterceptor 在代理方法里先 txManager.getTransaction(attr) 拿事务状态,再 invocation.proceed() 调目标方法,正常则 commit,抛异常则 rollback。七种传播行为中,默认 REQUIRED 是有事务则加入、无则新建;REQUIRES_NEW 调用 transactionManager.suspend() 把当前 Connection 从 ThreadLocal 移除、用新 Connection 开全新独立事务,子事务提交回滚与主事务无关;NESTED 不挂起不新建 Connection,只调 conn.setSavepoint(),子事务回滚回到保存点、主事务继续。失效六场景——this 调用、非 public、异常被吞、未配 rollbackFor(默认只回滚 RuntimeException)、多数据源、final 方法——核心都指向没有走到代理增强的方法上。

底层深入(5-10min)

Spring 事务的本质

@Transactional 不是魔法——它是基于 AOP 代理实现的。TransactionInterceptor 实现 AOP Alliance 的 MethodInterceptor,是事务增强的入口。真实源码:

@Override
public @Nullable Object invoke(MethodInvocation invocation) throws Throwable {
	// Work out the target class: may be {@code null}.
	// The TransactionAttributeSource should be passed the target class
	// as well as the method, which may be from an interface.
	Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

	// Adapt to TransactionAspectSupport's invokeWithinTransaction...
	return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
		@Override
		public @Nullable Object proceedWithInvocation() throws Throwable {
			return invocation.proceed();
		}
		@Override
		public void onRollback(Throwable failure, TransactionExecution execution) {
			MethodRollbackEvent event = new MethodRollbackEvent(invocation, failure, execution);
			logger.trace(event, failure);
			if (applicationEventPublisher != null) {
				try {
					applicationEventPublisher.publishEvent(event);
				}
				catch (Throwable ex) {
					if (logger.isWarnEnabled()) {
						logger.warn("Failed to publish " + event, ex);
					}
				}
			}
		}
	});
}

invoke() 先解析目标类,然后把真正的调用 invocation.proceed() 包装成一个 InvocationCallback 回调,交给父类 TransactionAspectSupport.invokeWithinTransaction() 处理。注意 invoke() 本身并不直接 commit/rollback——事务的开启、提交、回滚都在 invokeWithinTransaction() 里完成(那里才调 txManager.getTransaction()commit()rollback())。onRollback 回调说明回滚发生时还会发布一个 MethodRollbackEvent 事件,供监听器感知回滚。

💡 思考穿插:为什么说 @Transactional 的本质是 AOP 代理,而不是注解本身? 因为注解只是一条元数据,真正干活的是 Spring 启动时生成的代理对象——外部调用 service.pay() 时,实际进入的是代理的拦截器,它先开事务、再调目标方法、最后提交/回滚。一旦你绕过了这个代理、直接调原始对象,注解写得再正确也没人去执行。所以后面所有「失效场景」,本质都是「没走代理」。

7 种传播行为

传播行为含义
REQUIRED(默认)有事务则加入,无则新建
REQUIRES_NEW挂起当前事务,新建独立事务
NESTED在当前事务中创建 Savepoint(嵌套子事务)
SUPPORTS有事务则加入,无则无事务运行
NOT_SUPPORTED挂起当前事务,无事务运行
NEVER有事务则抛异常
MANDATORY无事务则抛异常

传播行为的源码判断:getTransaction

七种传播行为的判定都集中在 AbstractPlatformTransactionManager.getTransaction() 里。它先调 doGetTransaction() 拿当前连接上的事务资源,再用 isExistingTransaction() 判断「是否已有事务」,据此分成两条支路——已有事务走 handleExistingTransaction(),没有事务则按传播行为决定新建还是抛异常。真实源码:

@Override
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
		throws TransactionException {

	// Use defaults if no transaction definition given.
	TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults());

	Object transaction = doGetTransaction();
	boolean debugEnabled = logger.isDebugEnabled();

	if (isExistingTransaction(transaction)) {
		// Existing transaction found -> check propagation behavior to find out how to behave.
		return handleExistingTransaction(def, transaction, debugEnabled);
	}

	// Check definition settings for new transaction.
	if (def.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
		throw new InvalidTimeoutException("Invalid transaction timeout", def.getTimeout());
	}

	// No existing transaction found -> check propagation behavior to find out how to proceed.
	if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
		throw new IllegalTransactionStateException(
				"No existing transaction found for transaction marked with propagation 'mandatory'");
	}
	else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
			def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
			def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
		SuspendedResourcesHolder suspendedResources = suspend(null);
		if (debugEnabled) {
			logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
		}
		try {
			return startTransaction(def, transaction, false, debugEnabled, suspendedResources);
		}
		catch (RuntimeException | Error ex) {
			resume(null, suspendedResources);
			throw ex;
		}
	}
	else {
		// Create "empty" transaction: no actual transaction, but potentially synchronization.
		if (def.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
			logger.warn("Custom isolation level specified but no actual transaction initiated; " +
					"isolation level will effectively be ignored: " + def);
		}
		boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
		return prepareTransactionStatus(def, null, true, newSynchronization, debugEnabled, null);
	}
}

注意「无事务」分支的几个关键点:MANDATORY 直接抛异常;REQUIRED/REQUIRES_NEW/NESTED 都会走 startTransaction() 真正 doBegin() 开启事务;SUPPORTS/NOT_SUPPORTED/NEVER 落到最后的 else,返回一个「空事务状态」(没有真实连接事务,只有可能的同步)。这里 startTransaction() 最终调 doBegin(transaction, definition),而 doBeginDataSourceTransactionManager 等子类实现,负责真正从数据源拿 Connection 并设 autoCommit=false

传播行为的源码判断:handleExistingTransaction

「已有事务」时走 handleExistingTransaction(),这是 REQUIRES_NEWNESTED 差异最清晰的地方。真实源码:

private TransactionStatus handleExistingTransaction(
		TransactionDefinition definition, Object transaction, boolean debugEnabled)
		throws TransactionException {

	if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
		throw new IllegalTransactionStateException(
				"Existing transaction found for transaction marked with propagation 'never'");
	}

	if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
		if (debugEnabled) {
			logger.debug("Suspending current transaction");
		}
		Object suspendedResources = suspend(transaction);
		boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
		return prepareTransactionStatus(
				definition, null, false, newSynchronization, debugEnabled, suspendedResources);
	}

	if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
		if (debugEnabled) {
			logger.debug("Suspending current transaction, creating new transaction with name [" +
					definition.getName() + "]");
		}
		SuspendedResourcesHolder suspendedResources = suspend(transaction);
		try {
			return startTransaction(definition, transaction, false, debugEnabled, suspendedResources);
		}
		catch (RuntimeException | Error beginEx) {
			resumeAfterBeginException(transaction, suspendedResources, beginEx);
			throw beginEx;
		}
	}

	if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
		if (!isNestedTransactionAllowed()) {
			throw new NestedTransactionNotSupportedException(
					"Transaction manager does not allow nested transactions by default - " +
					"specify 'nestedTransactionAllowed' property with value 'true'");
		}
		if (debugEnabled) {
			logger.debug("Creating nested transaction with name [" + definition.getName() + "]");
		}
		if (useSavepointForNestedTransaction()) {
			// Create savepoint within existing Spring-managed transaction,
			// through the SavepointManager API implemented by TransactionStatus.
			// Usually uses JDBC savepoints. Never activates Spring synchronization.
			DefaultTransactionStatus status = newTransactionStatus(
					definition, transaction, false, false, true, debugEnabled, null);
			this.transactionExecutionListeners.forEach(listener -> listener.beforeBegin(status));
			try {
				status.createAndHoldSavepoint();
			}
			catch (RuntimeException | Error ex) {
				this.transactionExecutionListeners.forEach(listener -> listener.afterBegin(status, ex));
				throw ex;
			}
			this.transactionExecutionListeners.forEach(listener -> listener.afterBegin(status, null));
			return status;
		}
		else {
			// Nested transaction through nested begin and commit/rollback calls.
			// Usually only for JTA: Spring synchronization might get activated here
			// in case of a pre-existing JTA transaction.
			return startTransaction(definition, transaction, true, debugEnabled, null);
		}
	}

	// PROPAGATION_REQUIRED, PROPAGATION_SUPPORTS, PROPAGATION_MANDATORY:
	// regular participation in existing transaction.
	if (debugEnabled) {
		logger.debug("Participating in existing transaction");
	}
	if (isValidateExistingTransaction()) {
		if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
			Integer currentIsolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel();
			if (currentIsolationLevel == null || currentIsolationLevel != definition.getIsolationLevel()) {
				throw new IllegalTransactionStateException("Participating transaction with definition [" +
						definition + "] specifies isolation level which is incompatible with existing transaction: " +
						(currentIsolationLevel != null ?
								DefaultTransactionDefinition.getIsolationLevelName(currentIsolationLevel) :
								"(unknown)"));
			}
		}
		if (!definition.isReadOnly()) {
			if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
				throw new IllegalTransactionStateException("Participating transaction with definition [" +
						definition + "] is not marked as read-only but existing transaction is");
			}
		}
	}
	boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
	return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
}

对比两条支路:REQUIRES_NEWsuspend(transaction) 挂起当前事务(把当前 Connection 从 ThreadLocal 摘走),再用 startTransaction() 开一个全新的独立事务;NESTED 则不挂起、不换 Connection,走 status.createAndHoldSavepoint() 建保存点(useSavepointForNestedTransaction() 为 true 时,JDBC 场景即如此)。最后的 REQUIRED/SUPPORTS/MANDATORY 都落到「参与现有事务」,直接复用当前事务、只返回一个非新建的状态,这也是默认 REQUIRED 的行为。

REQUIRES_NEW vs NESTED 的核心区别

REQUIRES_NEW:调用 transactionManager.suspend() 挂起当前事务(将当前 Connection 从 ThreadLocal 移除),用新 Connection 开启全新独立事务。子事务的提交/回滚与主事务完全无关。

NESTED:不挂起事务,不新建 Connection。只是在当前事务中调用 conn.setSavepoint()。子事务回滚只回到保存点,主事务继续。要求底层数据库支持 Savepoint(MySQL InnoDB 支持)。

💡 思考穿插:为什么 REQUIRES_NEW 要「挂起 + 换新 Connection」,而 NESTED 用「同一个 Connection 的 Savepoint」? 因为两者要的独立性不同:REQUIRES_NEW 要的是「完全独立」——子事务提交回滚跟主事务毫无关系,所以必须换一条物理连接,否则主事务一回滚就把子事务一起带走了;NESTED 要的是「可部分回滚」——子事务只是主事务里的一个可回滚标记点,回滚时回到 Savepoint 而不是整个事务,主事务还能继续提交。一个换连接求独立,一个下标记求局部回滚。

@Transactional 失效的 6 种场景

1. 同类内 this.method() 调用

@Service
public class OrderService {
    public void create() {
        this.pay();  // 直接调目标对象,绕过了 AOP 代理
    }
    
    @Transactional
    public void pay() { }
}

this.pay() 调用的是原始对象的方法,不是代理对象的增强版本 → 事务拦截器未执行。解决方案:抽到另一个 Bean 注入后调用,或 AopContext.currentProxy()

💡 思考穿插:为什么同类内 this.method() 自调用,事务就失效了? 因为 this 指向的是被代理的原始对象本身,而不是 Spring 包装出来的代理对象——你调的是「目标方法」,跳过了代理的拦截逻辑。事务逻辑在代理的拦截器里,不在目标方法里,所以「内部直调」根本没经过代理,拦截器自然没机会开事务。解决办法就是让调用「走代理」:注入自己、AopContext.currentProxy()、或拆到另一个 Bean。

2. 非 public 方法

@Transactional
private void pay() { }  // private → CGLIB 无法拦截

CGLIB 通过继承生成代理子类,private 方法不可被子类重写。protected/package-private 同理。

💡 思考穿插:为什么 private 方法加 @Transactional 也没用? 因为 Spring 默认用 CGLIB「继承 + 重写」来生成代理,而 private 方法不能被继承重写,代理无从覆盖它,拦截器自然拦不到;同理 final 方法也不能被重写。代理机制决定了「能被重写」是事务生效的前提——注解再对,方法进不了代理也白搭。

3. 异常被 catch 吞掉

@Transactional
public void pay() {
    try {
        doPayment();
    } catch (Exception e) {
        log.error("支付失败", e);  // 吞掉异常 → 事务不感知 → 不触发回滚
    }
}

4. 未配置 rollbackFor

@Transactional  // 默认只回滚 RuntimeException
public void pay() throws IOException {
    // 抛 IOException(受检异常)→ 不会滚!
}

5. 多数据源

@Transactional  // 只管理当前事务管理器绑定的数据源
public void transfer() {
    db1.update();  // 事务1
    db2.update();  // 事务2 — 不在同一事务管理器中!
}

6. final 方法

@Transactional
public final void pay() { }  // CGLIB 无法重写 final 方法

总结

@Transactional 失效的根因几乎都是**“调用未经过 AOP 代理”**。理解 Spring 事务的本质——AOP 代理包裹 + TransactionInterceptor 拦截——就能快速定位所有失效原因。

章末提问

追问 1:@Transactional 为什么在同类内 this.method() 自调用时会失效?

结论先行:因为 this 是原始对象、不是代理对象,自调用没经过 AOP 代理,事务拦截器根本没执行。因为 事务是通过代理在方法前后「包裹」实现的,代理之外的直调自然绕过了事务。解决:注入代理自己、AopContext.currentProxy(),或把方法拆到另一个 Bean 里注入调用。

追问 2:REQUIRES_NEW 和 NESTED 的本质区别是什么?

结论先行:REQUIRES_NEW 挂起当前事务、换新 Connection 开完全独立事务,子事务与主事务互不影响;NESTED 不挂起、不换连接,用 Savepoint 实现「局部回滚」。因为 一个追求「独立提交/回滚」,一个追求「部分回滚后主事务继续」,独立性需求不同决定了实现手段不同——前者换连接,后者下保存点。

追问 3:为什么 @Transactional 默认只回滚 RuntimeException,受检异常要配 rollbackFor

结论先行:因为 Spring 沿用了 EJB 的历史约定——RuntimeException 表示「不可恢复的程序错误」默认回滚,受检异常表示「业务可处理的异常」默认不回滚。因为 受检异常常被当成业务流程的一部分(如文件不存在、校验失败),Spring 不愿替你决定要不要回滚,所以留给你用 rollbackFor 显式声明。


Share this post on:

Previous Post
工厂模式——Spring FactoryBean如何封装复杂Bean的创建
Next Post
Spring Boot自动装配——@SpringBootApplication背后的秘密