前言

本文主要分享一个常见的Spring框架中的事务管理问题,事务回滚异常@Transactional,分析原因以及解决方案

一、问题描述

设置事务手动回滚:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()

想返回比较友好的提示,对代码进行了try…catch,返回特定提示,但这样事务就不会回滚了,因为声明式事务是需要经过切面才会回滚的,所以在catch代码块中设置手动回滚,但出现了如下的异常:

2023-01-08 13:37:51.790 ERROR 5192 --- [nio-8880-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet]      : Servlet.service() for servlet [dispatcherServlet] in context with path [/ceshi] threw exception [Request processing failed; nested exception is org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only] with root cause

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) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:707) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:633) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:386) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
	at cn.lw.service.impl.ShiwuServiceImpl$$EnhancerBySpringCGLIB$$33a86b07.insertRobllBack(<generated>) ~[classes/:na]

二、原因分析

1、异常示例代码

以下代码的调用逻辑:insertRobllBack->forlanaService.insertForlanA->forlanBService.insertForlanB,每个方法上面都加了@Transactional,在insertForlanB方法中抛出了异常,但在insertForlanA方法捕获处理了,因为需要返回特定的异常,并设置了手动回滚

@Transactional
public String insertRobllBack(ForlanA forlanA, ForlanB forlanB) {
	return forlanaService.insertForlanA(forlanA);
}

@Transactional
public String insertForlanA(ForlanA forlanA) {
	try {
		forlanBService.insertForlanB(new ForlanB());
	} catch (Exception e) {
		e.printStackTrace();
		// 为了支持特定异常,设置手动回滚
		TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
		return "特定异常结果";
	}
	return "成功";
}

@Transactional
public void insertForlanB(ForlanB forlanB) {
	forlanBDao.insert(forlanB);
	int res = 1 / 0; //java.lang.ArithmeticException: / by zero
}

2、查看源码

主要来自于AbstractPlatformTransactionManager类的代码

@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);
}

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);
	}
}

上面的源码,最关键的就是下面这段代码,如果已经有全局回滚标记,我们再次进行commit的情况下,就会抛出异常UnexpectedRollbackException

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;
}

// 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");
}

If a participating transaction (e.g. with PROPAGATION_REQUIRED or PROPAGATION_SUPPORTS encountering an existing transaction) fails, the transaction will be globally marked as rollback-only

所以,原因很明显了,就是我们内部事务已经抛出异常了,此时,事务已经被标记为rollback-only,最外层事务还commit的话,这就有问题了

三、解决方案

1、修改内层事务传播特性为@Transactional(propagation = Propagation.REQUIRES_NEW) ,这样内层和外层分别使用了不同事务,就不影响了
2、try…catch写到最外层事务,但是记得在catch设置手动回滚,不然还是存在上面问题,具体可以查看针对跨方法异常捕获

3、不要try…catch,异常直接抛到最外层

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐