Transaction rolled back because it has been marked as rollback-only错误探究

背景:在一个dao插入数据到数据库时发生异常,捕获后打印日志,重新抛出一个SQL异常类到service层,service层捕获后处理,然后重新抛出一个带有自定义的message异常到controller层,然后将自定义的message抛到前台展示。但是前台展示的是Transaction rolled back because it has been marked as rollback-only信息。
业务代码如下?:
一个模版类
SyncExamDataClientTemplate.java

public final Map<String, Object> syncData(T examData, List<V> examSyncInfoList) throws SyncException,SyncSQLException {
    Map<String, Object> result;
    try {
        result = syncExamData(examData);
    }catch (Exception e){
        throw new SyncException(e);
    }
    if ((boolean)result.get("status")){
        try {
            updateSyncInfo(examSyncInfoList, result, examData);
        }catch (Exception e){
			  e.printStackTrace();
			  throw new SQLException();
           // throw new SyncSQLException(e);
        }
    }
    return result;
}

代码:updateSyncInfo(examSyncInfoList, result, examData);会执行insert操作,并且带有事务。
请注意代码:throw new SQLException();并没有抛出捕获到的异常e,而是抛出了一个新的异常。

service类:

try {
	List<ExamSyncInfo> planList = getExamSyncInfos(syncType);
	result = syncExamPlanDataClient.syncData(examPlan, planList);
} catch (SyncException e1) {
	logger.error("计划【" + examPlan.getId() + "】同步计划到考试能力平台异常", e1);
	throw new Exception("保存考试计划失败,原因:同步计划到考试能力平台失败!");
} catch (SQLException e2) {
	//如果同步成功但是插入数据库异常,则需要回滚该步骤操作(新增的计划),以下同理
	planDataRollBack(examPlan, syncType);
	throw new Exception("保存考试计划失败,原因:计划更新同步状态异常!");
}
if (!(boolean)result.get("status"))
	throw new Exception("保存考试计划失败,原因:同步计划到考试能力平台失败!");

上面的代码catch到了SQLException异常,然后抛出一个Exception异常到controller层。

controller类:

try {
	examPlanService.syncExamPlan(examPlan, ADD_PLAN);
} catch (Exception e) {
	e.printStackTrace();
	addMessage(redirectAttributes, e.getMessage());
	return "redirect:"+Global.getAdminPath()+"/examplan/examPlan/?repage";
}

由于service可能会catch到多种异常,所以想法是在service层将异常都转化为一种抛到controller层,这样在返回给前端的异常信息时,只需要e.getMessage()提取service层的异常信息即可。但是出现了上面所说的问题,如果出现数据库异常,那么前端应该得到“保存考试计划失败,原因:同步计划到考试能力平台失败!”这段提示,但实际上得到了标题所示的提示,究竟为何这样?请看下面?分解:

Spring源码追踪:
从service抛出异常到controller到底经历了什么?

1.MethodProxy.java

public Object invoke(Object obj, Object[] args) throws Throwable {
    try {
        this.init();
        MethodProxy.FastClassInfo fci = this.fastClassInfo;
        return fci.f1.invoke(fci.i1, obj, args);
    } catch (InvocationTargetException var4) {
        throw var4.getTargetException();
    } catch (IllegalArgumentException var5) {
        if (this.fastClassInfo.i1 < 0) {
            throw new IllegalArgumentException("Protected method: " + this.sig1);
        } else {
            throw var5;
        }
    }
}

该类应该是通过反射调用service里面方法。接着会执行这一句throw var4.getTargetException();,跳到2

2.进入到TransactionAspectSupport#invokeWithinTransaction方法,下面是该方法部分代码:

if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
	// Standard transaction demarcation with getTransaction and commit/rollback calls.
	TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
	Object retVal = null;
	try {
		// This is an around advice: Invoke the next interceptor in the chain.
		// This will normally result in a target object being invoked.
		retVal = invocation.proceedWithInvocation();
	}
	catch (Throwable ex) {
		// target invocation exception
		completeTransactionAfterThrowing(txInfo, ex);
		throw ex;
	}
	finally {
		cleanupTransactionInfo(txInfo);
	}
	commitTransactionAfterReturning(txInfo);
	return retVal;
}

这时进入completeTransactionAfterThrowing(txInfo, ex);代码,*从该方法可以看出应该是在异常抛出后要先完成事务,也就是说在异常抛出事务定义的边界之前会先完成事务。*让我们进入该方法查看:

3.TransactionAspectSupport#completeTransactionAfterThrowing方法,下面是代码:

protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {
	if (txInfo != null && txInfo.hasTransaction()) {
		if (logger.isTraceEnabled()) {
			logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
					"] after exception: " + ex);
		}
		if (txInfo.transactionAttribute.rollbackOn(ex)) {
			try {
				txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
			}
			catch (TransactionSystemException ex2) {
				logger.error("Application exception overridden by rollback exception", ex);
				ex2.initApplicationException(ex);
				throw ex2;
			}
			catch (RuntimeException ex2) {
				logger.error("Application exception overridden by rollback exception", ex);
				throw ex2;
			}
			catch (Error err) {
				logger.error("Application exception overridden by rollback error", ex);
				throw err;
			}
		}
		else {
			// We don’t roll back on this exception.
			// Will still roll back if TransactionStatus.isRollbackOnly() is true.
			try {
				txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
			}
			catch (TransactionSystemException ex2) {
				logger.error("Application exception overridden by commit exception", ex);
				ex2.initApplicationException(ex);
				throw ex2;
			}
			catch (RuntimeException ex2) {
				logger.error("Application exception overridden by commit exception", ex);
				throw ex2;
			}
			catch (Error err) {
				logger.error("Application exception overridden by commit error", ex);
				throw err;
			}
		}
	}
}

代码会接着执行到if (txInfo.transactionAttribute.rollbackOn(ex))这句代码,txInfo.transactionAttribute应该是事务配置的属性,接着进入rollbackOn方法查看:

4.rollbackOn方法定义在TransactionAttribute接口中,源码如下:

/**
 * Should we roll back on the given exception?
 * @param ex the exception to evaluate
 * @return whether to perform a rollback or not
 */
boolean rollbackOn(Throwable ex);

从注释可以看出该方法主要是判断对该异常是否执行回滚。接着进入该方法实现代码:

@Override
public boolean rollbackOn(Throwable ex) {
	return this.targetAttribute.rollbackOn(ex);
}

继续进入:
RuleBasedTransactionAttribute#rollbackOn方法:

/**
 * Winning rule is the shallowest rule (that is, the closest in the
 * inheritance hierarchy to the exception). If no rule applies (-1),
 * return false.
 * @see TransactionAttribute#rollbackOn(java.lang.Throwable)
 */
@Override
public boolean rollbackOn(Throwable ex) {
	if (logger.isTraceEnabled()) {
		logger.trace("Applying rules to determine whether transaction should rollback on " + ex);
	}

	RollbackRuleAttribute winner = null;
	int deepest = Integer.MAX_VALUE;

	if (this.rollbackRules != null) {
		for (RollbackRuleAttribute rule : this.rollbackRules) {
			int depth = rule.getDepth(ex);
			if (depth >= 0 && depth < deepest) {
				deepest = depth;
				winner = rule;
			}
		}
	}

	if (logger.isTraceEnabled()) {
		logger.trace("Winning rollback rule is: " + winner);
	}

	// User superclass behavior (rollback on unchecked) if no rule matches.
	if (winner == null) {
		logger.trace("No relevant rollback rule found: applying default rules");
		return super.rollbackOn(ex);
	}

	return !(winner instanceof NoRollbackRuleAttribute);
}

从注释知道:判断是否回滚的胜负法则是:就近原则(也就是在继承层次中离异常最近的),什么意思?
this.rollbackRules代表了一些回滚的规则,rule.getDepth(ex);是找到该异常在规则中的深度,通过源码:

public int getDepth(Throwable ex) {
	return getDepth(ex.getClass(), 0);
}


private int getDepth(Class<?> exceptionClass, int depth) {
	if (exceptionClass.getName().contains(this.exceptionName)) {
		// Found it!
		return depth;
	}
	// If we’ve gone as far as we can go and haven’t found it…
	if (exceptionClass == Throwable.class) {
		return -1;
	}
	return getDepth(exceptionClass.getSuperclass(), depth + 1);
}

可看出来:寻找异常在规则中的深度,也就是说如果该异常名字字符串包含规则中定义的异常名字字符串,例如抛出异常为SyncSQLException,而规则中的异常为Exception,则找到。否则递归到抛出异常的父类中寻找,如果找不到返回-1

回到上面调用getDepth方法的代码,所以胜负规则是找到一个规则:该规则定义的异常与抛出的异常最接近。然后判断是否需要回滚。

如果winner为null,则进入父类中寻找规则:
DefaultTransactionAttribute#rollbackOn方法:

/**
 * The default behavior is as with EJB: rollback on unchecked exception.
 * Additionally attempt to rollback on Error.
 * <p>This is consistent with TransactionTemplate’s default behavior.
 */
@Override
public boolean rollbackOn(Throwable ex) {
	return (ex instanceof RuntimeException || ex instanceof Error);
}

默认规则是,如果抛出的异常是RuntimeExceptionError的实例,那么返回true,也就是说进行回滚。
那么我们是否需要回滚呢?答案是肯定的。因为从上面第3步方法可以看出:
如果回滚,则符合一般规则:数据库异常回滚。
否则:执行commit操作
应该是commit时报的上面的错误,那么现在进入commit源码:
AbstractPlatformTransactionManager#commit方法:

/**
 * This implementation of commit handles participating in existing
 * transactions and programmatic rollback requests.
 * Delegates to {@code isRollbackOnly}, {@code doCommit}
 * and {@code rollback}.
 * @see org.springframework.transaction.TransactionStatus#isRollbackOnly()
 * @see #doCommit
 * @see #rollback
 */
@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);
		return;
	}
	if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
		if (defStatus.isDebug()) {
			logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
		}
		processRollback(defStatus);
		// Throw UnexpectedRollbackException only at outermost transaction boundary
		// or if explicitly asked to.
		if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
			throw new UnexpectedRollbackException(
					"Transaction rolled back because it has been marked as rollback-only");
		}
		return;
	}

	processCommit(defStatus);
}

接着进入defStatus.isGlobalRollbackOnly()方法:
DefaultTransactionStatus#isGlobalRollbackOnly方法:

/**
 * Determine the rollback-only flag via checking both the transaction object,
 * provided that the latter implements the {@link SmartTransactionObject} interface.
 * <p>Will return "true" if the transaction itself has been marked rollback-only
 * by the transaction coordinator, for example in case of a timeout.
 * @see SmartTransactionObject#isRollbackOnly
 */
@Override
public boolean isGlobalRollbackOnly() {
	return ((this.transaction instanceof SmartTransactionObject) &&
			((SmartTransactionObject) this.transaction).isRollbackOnly());
}

this.transaction是事务管理器的一个对象,代表一个数据库连接,这里我们项目中是DataSourceTransactionObject,它是继承与SmartTransactionObject类的。那么进入isRollbackOnly方法,
ResourceHolderSupport#isRollbackOnly方法:发现返回的是true。为什么是true呢?因为在Spring事务中如果有非受检异常发生,那么整个事务会被标记为rollback-only。

回到调用isGlobalRollbackOnly()方法的地方,也就是commit方法内,继续执行processRollback(defStatus);方法进行回滚,然后进行下面代码:

// Throw UnexpectedRollbackException only at outermost transaction boundary
// or if explicitly asked to.
if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {
	throw new UnexpectedRollbackException(
			"Transaction rolled back because it has been marked as rollback-only");
}

通过注释可以知道:仅在最外层的事务边界上抛出UnexpectedRollbackException,或者如果有明确要求。
如果status.isNewTransaction()为true,那么到达事务边界,会抛出UnexpectedRollbackException。
也就是我们在前端得到的异常。

至此,我们应该知道报错原因了:
数据库异常抛出了一个非受检异常,但是我们将它消化了,没有抛出去,而是抛出了一个SQLException到service层,在service层我们同样抛出了一个Exception。由于是受检异常,那么会执行commit方法,由于前面说的事务被设置了全局rollback-only,于是commit并不会进行正常commit,而是进行rollback,并且在事务边界到达时抛出了UnexpectedRollbackException异常。

那么我们可以知道解决方案了,那就是:
1.自定义一个继承RuntimeException类的业务异常
2.将该业务异常抛出到service层
3.将一个新的拥有自定义报错信息业务异常继续抛出事务边界:到controller层
4.controller层将自定义报错信息抛到前端显示

总结:
解决该异常的方法:
第一种:将捕获到的非受检异常抛出到事务边界外,而不是消化它。
第二种:如果要自己消化它,那么请自定义一个新的非受检异常抛出到事务边界,这样就能正确执行rollback,而不是在异常时执行commit操作。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值