执行报错:Transaction rolled back because it has been marked as rollback-only

背景

业务在执行时,出现报错,日志如下所示:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:870)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:707)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)

模拟代码如下所示:


package com.example.demo.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Sl4fj
@Service
public class DemoService {

    @Transactional
    public void outerMethod() {
        try {
            // 调用内部方法
            innerMethod();
        } catch (Exception e) {
            // 捕获内部方法的异常,但不处理或不重新抛出
            log.error("Caught exception in outerMethod: {}" , e.getMessage());
        }

        // 执行其他操作
        log.info("Continuing execution in outerMethod.");
    }

    @Transactional
    public void innerMethod() {
        // 执行一些数据库操作
        log.info("Executing innerMethod.");

        // 模拟异常
        throw new RuntimeException("Simulated exception in innerMethod.");
    }
}

当运行这段伪代码时,控制台输出如下:

Executing innerMethod.
Caught exception in outerMethod: Simulated exception in innerMethod.
Continuing execution in outerMethod.
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
....

原因分析

查看异常信息,我们知道 UnexpectedRollbackException 是从类 AbstractPlatformTransactionManager.java 的 line 870 抛出的,源码如下:

	/**
	 * Process an actual rollback.
	 * The completed flag has already been checked.
	 * @param status object representing the transaction
	 * @throws TransactionException in case of rollback failure
	 */
	private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
		try {
			boolean unexpectedRollback = unexpected;

			try {
				triggerBeforeCompletion(status);

				if (status.hasSavepoint()) {
					if (status.isDebug()) {
						logger.debug("Rolling back transaction to savepoint");
					}
					status.rollbackToHeldSavepoint();
				}
				else if (status.isNewTransaction()) {
					if (status.isDebug()) {
						logger.debug("Initiating transaction rollback");
					}
					doRollback(status);
				}
				else {
					// Participating in larger transaction
					if (status.hasTransaction()) {
						if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
							if (status.isDebug()) {
								logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
							}
							doSetRollbackOnly(status);
						}
						else {
							if (status.isDebug()) {
								logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
							}
						}
					}
					else {
						logger.debug("Should roll back transaction but cannot - no transaction available");
					}
					// Unexpected rollback only matters here if we're asked to fail early
					if (!isFailEarlyOnGlobalRollbackOnly()) {
						unexpectedRollback = false;
					}
				}
			}
			catch (RuntimeException | Error ex) {
				triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
				throw ex;
			}

			triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

			// Raise UnexpectedRollbackException if we had a global rollback-only marker
			if (unexpectedRollback) {
				throw new UnexpectedRollbackException(
						"Transaction rolled back because it has been marked as rollback-only");
			}
		}
		finally {
			cleanupAfterCompletion(status);
		}
	}


由此可见,内部事务因为抛异常,已经把事务标记为 rollback-only。而 unexpectedRollback 为 true,则是由调用方传入的,查看调用方 AbstractPlatformTransactionManager.commit 代码可知,直接传入了 processRollback(defStatus, true):

	@Override
	public final void commit(TransactionStatus status) throws TransactionException {
		if (status.isCompleted()) {
			throw new IllegalTransactionStateException(
					"Transaction is already completed - do not call commit or rollback more than once per transaction");
		}

		DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
      // 如果在事务链中已经被标记回滚,那么不会尝试提交事务,直接回滚
		if (defStatus.isLocalRollbackOnly()) {
			if (defStatus.isDebug()) {
				logger.debug("Transactional code has requested rollback");
			}
			processRollback(defStatus, false);
			return;
		}

		if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
			if (defStatus.isDebug()) {
				logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
			}
           // 进行事务回滚,并且抛出一个异常
			processRollback(defStatus, true);
			return;
		}
       // 没有被标记为回滚,这里才真正判断是否提交
		processCommit(defStatus);
	}


因此,通过上面的源码分析,我们可以得知如下结论:

  1. 事务传播行为
    在 Spring 中,@Transactional 注解的方法默认使用传播属性 Propagation.REQUIRED。
    这意味着如果当前没有事务,则创建一个新事务;如果当前存在事务,则加入当前事务。
  2. 异常处理
    innerMethod() 方法抛出一个运行时异常 RuntimeException。
    在 outerMethod() 中,我们调用 innerMethod(),并在 try-catch 块中捕获了异常,但没有重新抛出。
    由于异常被捕获且未重新抛出,事务管理器无法感知到异常的发生。
  3. 事务回滚标记
    当 innerMethod() 中发生未捕获的运行时异常时,Spring 的事务管理器会标记当前事务为回滚状态(rollback-only)。
    即使异常被捕获,事务仍然被标记为回滚。
  4. 事务提交阶段
    在 outerMethod() 中,由于异常被捕获,程序继续执行,未抛出异常。
    当 outerMethod() 执行完毕,事务管理器尝试提交事务时,发现事务已被标记为回滚(rollback-only)。
    事务管理器抛出 UnexpectedRollbackException,提示 “Transaction rolled back because it has been marked as rollback-only”。

解决

  1. 让异常向上抛出
    不捕获异常:在 outerMethod() 中,不捕获 innerMethod() 抛出的异常,让其向上抛出。
    修改 outerMethod():
@Transactional
public void outerMethod() {
    innerMethod();
    // 其他操作
}

重新抛出异常:如果需要捕获异常,可以在捕获后重新抛出。

@Transactional
public void outerMethod() {
    try {
        innerMethod();
    } catch (Exception e) {
        // 处理后重新抛出
        System.out.println("Handling exception and rethrowing.");
        throw e;
    }
}

  1. 独立事务处理
    更改事务传播行为:将 innerMethod() 的事务传播属性设置为 Propagation.REQUIRES_NEW,使其在新事务中执行,内部事务的回滚不会影响外部事务。
    修改 innerMethod():
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void innerMethod() {
    // 执行一些数据库操作
    System.out.println("Executing innerMethod.");

    // 模拟异常
    throw new RuntimeException("Simulated exception in innerMethod.");
}
  1. 手动设置事务回滚和处理异常
    在捕获异常后手动设置事务回滚状态:如果必须捕获异常,可以在捕获后手动将事务标记为回滚,并根据需要处理异常。
    修改 innerMethod():
@Transactional
public void innerMethod() {
    try {
        // 执行一些数据库操作
        System.out.println("Executing innerMethod.");

        // 模拟异常
        throw new RuntimeException("Simulated exception in innerMethod.");
    } catch (Exception e) {
        // 手动标记事务为回滚
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        // 处理异常
        System.out.println("Exception in innerMethod: " + e.getMessage());
        // 根据需要重新抛出异常
    }
}

同时,在 outerMethod() 中,可以检测事务状态:

@Transactional
public void outerMethod() {
    innerMethod();

    if (TransactionAspectSupport.currentTransactionStatus().isRollbackOnly()) {
        // 事务已被标记为回滚,处理逻辑
        System.out.println("Transaction is marked as rollback-only.");
        // 根据需要抛出异常或进行其他处理
    }

    // 执行其他操作
    System.out.println("Continuing execution in outerMethod.");
}

总结

原因:在嵌套事务中,内部方法发生异常并被捕获,事务被标记为回滚状态,但异常未向上抛出。外部方法继续正常执行,直到事务提交时,事务管理器发现事务已被标记为回滚,抛出 Transaction rolled back because it has been marked as rollback-only 异常。

注意事项:

异常传播:在事务方法中,谨慎处理异常。默认情况下,应让运行时异常向上抛出,以便事务管理器感知并处理事务回滚。
事务传播属性:根据业务需要,正确设置事务的传播行为,避免不必要的事务嵌套和传播问题。
手动控制事务状态:在特殊情况下,可以手动设置事务的回滚状态,但需要确保外部方法能够感知并正确处理事务状态。
最佳实践:

不要吞噬异常:避免在事务方法中捕获异常而不处理或不重新抛出,防止事务状态和业务逻辑出现不一致。
统一异常处理:建立统一的异常处理机制,确保事务和业务逻辑的正确性。
事务设计:根据业务需求,合理划分事务边界,避免过大的事务范围导致的性能和一致性问题。

Reference

事务异常:Transaction rolled back because it has been marked as rollback-only - Su 的技术博客
java - UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only - Stack Overflow

@Transactional 嵌套事务失效异常 Transaction rolled back because it has been marked as rollback-only - 楼兰胡杨 - 博客园

posted @ 2024-09-27 09:25  Reecelin  阅读(329)  评论(0编辑  收藏  举报