执行报错: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);
}
因此,通过上面的源码分析,我们可以得知如下结论:
- 事务传播行为
在 Spring 中,@Transactional 注解的方法默认使用传播属性 Propagation.REQUIRED。
这意味着如果当前没有事务,则创建一个新事务;如果当前存在事务,则加入当前事务。 - 异常处理
innerMethod() 方法抛出一个运行时异常 RuntimeException。
在 outerMethod() 中,我们调用 innerMethod(),并在 try-catch 块中捕获了异常,但没有重新抛出。
由于异常被捕获且未重新抛出,事务管理器无法感知到异常的发生。 - 事务回滚标记
当 innerMethod() 中发生未捕获的运行时异常时,Spring 的事务管理器会标记当前事务为回滚状态(rollback-only)。
即使异常被捕获,事务仍然被标记为回滚。 - 事务提交阶段
在 outerMethod() 中,由于异常被捕获,程序继续执行,未抛出异常。
当 outerMethod() 执行完毕,事务管理器尝试提交事务时,发现事务已被标记为回滚(rollback-only)。
事务管理器抛出 UnexpectedRollbackException,提示 “Transaction rolled back because it has been marked as rollback-only”。
解决
- 让异常向上抛出
不捕获异常:在 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;
}
}
- 独立事务处理
更改事务传播行为:将 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.");
}
- 手动设置事务回滚和处理异常
在捕获异常后手动设置事务回滚状态:如果必须捕获异常,可以在捕获后手动将事务标记为回滚,并根据需要处理异常。
修改 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