在平时使用spring事务去开发的时候,可能我们会碰到如下图的一个异常:
这个异常是怎么回事?下面就让我们来探究一番
spring事务中的本地&全局事务
在分析上面的异常之前,我们先来了解下什么是spring的本地事务与全局事务。本地事务与全局事务的概念其实在分布式事务中谈论得比较广泛,但是其实在非分布式场景下,也就是我们得spring事务中也有所谓的本地事务与全局事务,只不过在一般场景来说我们可能并不会去关注到。在spring事务中,本地事务与全局事务是针对与嵌套事务的场景才会存在,举个例子,在UserService中会去调用OrderService,所以UserService会先开启一个事务,在调用OrderService的时候,OrderService也会开启一个事务,而由于UserService已经开启了事务了,所以OrderService的事务会作为一个嵌套事务,在这里,由UserService发起的事务可以称为全局事务,不过UserService这个事务又是整个事务的一部分,所以它其实也可以算是一个属于UserService它自己的本地事务,而OrderService就不用多说了,它自然就是一个本地事务
产生异常的场景
当我们两个事务的事务传播级别都是PROPAGATION_REQUIRED的时候,如果嵌套事务发生了异常但是被上层调用方catch掉了异常,也就是说如果上面的OrderService抛出了异常,但是被UserService进行了catch,这时候就会抛出Transaction rolled back because it has been marked as rollback-only,抛出的异常代码如下:
public void saveUser(User user) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
transactionTemplate.setName("saveUser");
transactionTemplate.execute(status -> {
try {
userDao.saveUser(user);
saveUser11(new User("kyrie"));
} catch (Exception e) {
}
return Boolean.TRUE;
});
}
public void saveUser11(User user) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setName("saveUser11");
transactionTemplate.execute(status -> {
userDao.saveUser(user);
int result = 1 / 0;
return Boolean.TRUE;
});
}
从源码层面去寻找异常源头
我们还是以上面的UserService和OrderService为例子,当OrderService抛出异常之后,会被spring事务进行catch,然后由事务管理器执行rollback方法进行回滚
org.springframework.transaction.support.AbstractPlatformTransactionManager#rollback
/**
* 对事务进行回滚
*
* @see #doRollback
* @see #doSetRollbackOnly
*/
@Override
public final void rollback(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;
// 真正执行事务回滚
processRollback(defStatus, false);
}
/**
* 真正执行事务回滚
*
* @param status 事务状态对象
* @param unexpected unexpected
* @throws TransactionException in case of rollback failure
*/
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;
try {
// 执行当前线程上下文中所有的事务同步器的beforeCompletion钩子方法
triggerBeforeCompletion(status);
// 条件成立:说明这个事务中设置了保存点
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Rolling back transaction to savepoint");
}
// 此时事务需要回滚到保存点的位置
status.rollbackToHeldSavepoint();
}
// 条件成立:说明此spring事务创建了数据库的物理事务
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction rollback");
}
// 真正回滚,子类具体实现
doRollback(status);
}
// 条件成立:当前执行的事务方法并没有创建新事务,也就是说该事务方法当前正在使用其他事务方法所创建的事务
else {
// 条件成立:说明该事务不是一个“空事务”
if (status.hasTransaction()) {
// 条件成立:1.status.isLocalRollbackOnly() == true, 表示当前执行的事务方法需要让它所在的事务强制回滚
// 2.isGlobalRollbackOnParticipationFailure() == true,表示正在执行的事务方法失败之后(也就是部分失败)整个事务都要回滚
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
if (status.isDebug()) {
logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
}
// 设置全局事务回滚标记,当事务发起方法进行commit的时候会检查这个标记
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;
}
// 执行同步器钩子synchronization.afterCompletion
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);
}
}
由于是OrderService产生了异常,虽然执行了事务管理器的rollback方法,但是并不会真正地去对事务进行回滚(因为UserService与OrderService都是属于同一个数据库事务,所以回滚也是由UserService进行回滚),此时代码就会来到上面processRollback方法的第37行
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
// ......
}
在这里会有两个判断条件,第一个从名字可以看出是判断当前OrderService本地事务是否标记了回滚,一般来说这里都是没有被标记的(怎样才会被标记后面说),这时候就需要看第二个判断条件了:
/**
* true表示部分失败之后整个事务都要回滚,而false则表示部分失败之后不影响整个事务的提交
*/
private boolean globalRollbackOnParticipationFailure = true;
/**
* 是否在参与的事务失败后将现有事务全局标记为只回滚,也就是说是否允许部分失败之后整个事务都进行回滚,true表示部分失败之后整个事务都要回滚,而false则表示部分失败之后不影响整个事务的提交
*/
public final boolean isGlobalRollbackOnParticipationFailure() {
return this.globalRollbackOnParticipationFailure;
}
通过名字可以知道这个方法就是判断当有本地事务抛出异常了之后,全局事务是否需要回滚,默认是true,表示本地事务全局事务需要回滚,所以就会执行doSetRollbackOnly(status)方法
protected void doSetRollbackOnly(DefaultTransactionStatus status) {
// 获取到事务对象
DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
if (status.isDebug()) {
logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() +
"] rollback-only");
}
// 给事务对象中的connection包装对象标记全局事务强制回滚
txObject.setRollbackOnly();
}
在doSetRollbackOnly方法中会去调用DataSourceTransactionObject的setRollbackOnly方法
public void setRollbackOnly() {
getConnectionHolder().setRollbackOnly();
}
在setRollbackOnly方法中又去调用DataSourceTransactionObject对象里面的ConnectionHolder的setRollbackOnly方法
/**
* 当前资源是否是全局仅回滚状态
*/
private boolean rollbackOnly = false;
/**
* 将资源事务标记为全局仅回滚
*/
public void setRollbackOnly() {
this.rollbackOnly = true;
}
在ConnectionHolder对象的setRollbackOnly方法中,会把一个rollbackOnly字段设置为true,而这个rollbackOnly字段就是用来标记该connection是否需要回滚
这里说一下DataSourceTransactionObject对象与ConnectionHolder对象的关系,每开启一个spring事务都会创建一个DataSourceTransactionObject对象,UserService会有一个,OrdreService也会有一个,他们的DataSourceTransactionObject对象都是彼此独立的,ConnectionHolder对象会包装jdbc的connection连接对象,由于UserService和OrderService它俩的事务传播级别都是PROPAGATION_REQUIRED,所以它俩会共用同一个connection连接对象,也就是说UserService和OrderService虽然它们的DataSourceTransactionObject对象是独立的,但是这两个DataSourceTransactionObject对象所引用的connection对象则是相同的
到这里OrderService的回滚操作就算是完成了,此时异常也抛给了UserService,但是UserService会把这个异常给catch住,导致最终执行事务管理器的commit方法试图把整个事务进行提交
org.springframework.transaction.support.AbstractPlatformTransactionManager#commit
/**
* 这里需要注意的就是,尽管这是一个commit方法,但是并不意味这一定会commit,这里面也可能会调用到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;
// 如果这里进行commit的是参与事务方法,那么在执行processRollback方法中会给整个事务标上全局仅回滚标记
// 如果这里进行commit的是发起事务方法,那么在执行processRollback方法中会执行真正的回滚方法
// 比如说有一个场景,在一个事务方法中,先执行了sql语句,然后发起了一个rpc调用,但此时可以由于网络原因发生了调用异常,
// 而我们希望返回一个友好的响应给前端,那么就在发起rpc调用的代码上加上try catch语句块捕获异常并直接返回一个友好的错误响应,
// 那么此时整个事务方法执行完是没有抛出异常的,所以一开始的时候执行的sql语句就直接commit了,但是很明显这种情况也是需要回滚的
// 所以此时强制回滚就显得很重要了,我们可以在catch中设置一下强制回滚的标记等于true,那么在整个事务方法commit的时候此条件就会成立
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
// 执行回滚方法
processRollback(defStatus, false);
return;
}
// shouldCommitOnGlobalRollbackOnly默认都会返回false,而defStatus.isGlobalRollbackOnly()什么时候会等于true?
// 如果发起事务的方法调用另一个参与事务方法,而这个参与事务方法抛出了异常,发起事务方法把异常catch住了,
// 这种情况下这个参与事务方法会进入回滚逻辑,在processRollback方法中根据globalRollbackOnParticipationFailure这个属性的设置去判断是否需要设置全局事务的回滚,
// 如果globalRollbackOnParticipationFailure == true,那么defStatus.isGlobalRollbackOnly()就会等于true,事务发起者commit事务的时候便会进入processRollback方法执行整个事务的回滚
// 否则globalRollbackOnParticipationFailure == false,那么defStatus.isGlobalRollbackOnly()就会等于false,下面的条件不会成立,整个事务commit成功
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);
}
当代码执行到第24行的时候,会判断UserService的本地事务是否标记了回滚,很明显是没有标记的(OrderService标记了),这时候条件不成立,代码继续执行到第38行,这时候这里就又有两个判断条件了,一个是shouldCommitOnGlobalRollbackOnly方法
protected boolean shouldCommitOnGlobalRollbackOnly() {
return false;
}
这个方法通过名字可以知道,就是当全局事务标记了回滚的时候,是否还需要提交,默认是false,表示不提交,如果全局事务标记了回滚就不允许提交了,那接下来自然就需要判断全局事务是否标记了回滚,如果标记了,那么就执行processRollback方法,并且processRollback方法的第二个参数传的是true,这时候我们继续回到processRollback方法中
/**
* 真正执行事务回滚
*
* @param status 事务状态对象
* @param unexpected unexpected
* @throws TransactionException in case of rollback failure
*/
private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
try {
boolean unexpectedRollback = unexpected;
try {
// 执行当前线程上下文中所有的事务同步器的beforeCompletion钩子方法
triggerBeforeCompletion(status);
// 条件成立:说明这个事务中设置了保存点
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Rolling back transaction to savepoint");
}
// 此时事务需要回滚到保存点的位置
status.rollbackToHeldSavepoint();
}
// 条件成立:说明此spring事务创建了数据库的物理事务
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction rollback");
}
// 真正回滚,子类具体实现
doRollback(status);
}
// 条件成立:当前执行的事务方法并没有创建新事务,也就是说该事务方法当前正在使用其他事务方法所创建的事务
else {
// 条件成立:说明该事务不是一个“空事务”
if (status.hasTransaction()) {
// 条件成立:1.status.isLocalRollbackOnly() == true, 表示当前执行的事务方法需要让它所在的事务强制回滚
// 2.isGlobalRollbackOnParticipationFailure() == true,表示正在执行的事务方法失败之后(也就是部分失败)整个事务都要回滚
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
if (status.isDebug()) {
logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
}
// 设置全局事务回滚标记,当事务发起方法进行commit的时候会检查这个标记
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;
}
// 执行同步器钩子synchronization.afterCompletion
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);
}
}
此时会执行第29行的代码,在doRollback方法中进行整个数据库事务的回滚,然后紧接着就会来到第65行,这里会判断unexpectedRollback这个变量,而unexpectedRollback这个变量则是由processRollback方法的第二个参数传过来的,传过来的正好就是true,所以if条件满足,抛出了一个异常,仔细一看,这个异常正是我们要寻找的Transaction rolled back because it has been marked as rollback-only异常
产生异常的原因
既然我们已经找到了异常发生的源头了,那么我们就可以顺着源码去看怎么解决这个异常了,但是现在我们还不知道这个异常到底是什么意思,spring为什么要给我们抛出这样的一个异常呢?接下来我们来分析一下。通过字面意思Transaction rolled back because it has been marked as rollback-only的意思就是说事务已经被标记为回滚状态了,还记得OrderService在抛出异常之后执行事务管理器的rollback方法吗,在rollback方法中会把connection级别(也就是全局事务级别)的事务标记了回滚状态,而当UserService准备去提交事务的时候,会去判断全局事务是否被标记了rollback,如果标记了,那么在commit方法中就不会再去执行提交事务的动作了,而是转头去执行processRollback方法去回滚事务,并且回滚事务的时候会在第二个unexpected参数中传true,关键就在于这个unexpected参数,因为它是导致异常是否抛出的直接原因,通过这个参数的名称可以知道,意思是不期望的,而此时执行的又是processRollback方法,所以连在一起就是表示不期望被回滚,但是最终还是被回滚了,其实言外之意就是虽然最终还是回滚了,但是spring并不希望我们这样去操作事务,就会抛出一个异常出来告知用户,那反过来,如果unexpected参数传的是false,那就是spring认为是正确的回滚事务的方式
如何解决这个异常
/**
* 这里需要注意的就是,尽管这是一个commit方法,但是并不意味这一定会commit,这里面也可能会调用到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;
// 如果这里进行commit的是参与事务方法,那么在执行processRollback方法中会给整个事务标上全局仅回滚标记
// 如果这里进行commit的是发起事务方法,那么在执行processRollback方法中会执行真正的回滚方法
// 比如说有一个场景,在一个事务方法中,先执行了sql语句,然后发起了一个rpc调用,但此时可以由于网络原因发生了调用异常,
// 而我们希望返回一个友好的响应给前端,那么就在发起rpc调用的代码上加上try catch语句块捕获异常并直接返回一个友好的错误响应,
// 那么此时整个事务方法执行完是没有抛出异常的,所以一开始的时候执行的sql语句就直接commit了,但是很明显这种情况也是需要回滚的
// 所以此时强制回滚就显得很重要了,我们可以在catch中设置一下强制回滚的标记等于true,那么在整个事务方法commit的时候此条件就会成立
if (defStatus.isLocalRollbackOnly()) {
if (defStatus.isDebug()) {
logger.debug("Transactional code has requested rollback");
}
// 执行回滚方法
processRollback(defStatus, false);
return;
}
// shouldCommitOnGlobalRollbackOnly默认都会返回false,而defStatus.isGlobalRollbackOnly()什么时候会等于true?
// 如果发起事务的方法调用另一个参与事务方法,而这个参与事务方法抛出了异常,发起事务方法把异常catch住了,
// 这种情况下这个参与事务方法会进入回滚逻辑,在processRollback方法中根据globalRollbackOnParticipationFailure这个属性的设置去判断是否需要设置全局事务的回滚,
// 如果globalRollbackOnParticipationFailure == true,那么defStatus.isGlobalRollbackOnly()就会等于true,事务发起者commit事务的时候便会进入processRollback方法执行整个事务的回滚
// 否则globalRollbackOnParticipationFailure == false,那么defStatus.isGlobalRollbackOnly()就会等于false,下面的条件不会成立,整个事务commit成功
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);
}
回到commit方法,我们可以看到,如果第一个if判断,会去判断当前提交事务时的这个本地事务是否被标记了回滚,如果是,那么也会执行processRollback方法,并且unexpected参数传的是false,既然传的是false,那么就不会抛出Transaction rolled back because it has been marked as rollback-only异常了,那么怎样才能让提交事务时的这个本地事务被标记为回滚呢?这就要区分你使用的是声明式事务还是编程时事务了
(1)声明式事务
@Transactional(rollbackFor = Exception.class)
public void saveUserByTransactional(User user) {
try {
userDao.saveUser(user);
orderService.saveOrder();
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
只需要在UserService捕获到OrderService的catch块中加上TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();这行代码,就可以把UserService这个本地事务标记为回滚状态了
(2)编程式事务
public void saveUser(User user) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
transactionTemplate.setName("saveUser");
transactionTemplate.execute(status -> {
try {
userDao.saveUser(user);
orderService.saveOrder();
} catch (Exception e) {
status.setRollbackOnly();
}
return Boolean.TRUE;
});
}
在声明式事务中会得到TransactionStatus对象,这时候就可以直接在捕获到异常之后调用TransactionStatus的setRollbackOnly方法把UserService的本地事务标记为回滚了
总结
当两个嵌套的spring事务的事务传播级别都是PROPAGATION_REQUIRED的时候,如果内层事务抛出了异常但是被外层事务catch住了,这种外层事务就会提交整个全局事务,在提交的时候由于内层事务产生了异常,所以默认情况下外层事务在提交全局事务的时候会进行rollback而不是commit(如果有需要也可以通过重写事务管理器的shouldCommitOnGlobalRollbackOnly方法去进行commit),而在rollback后spring在框架层面还会抛出一个Transaction rolled back because it has been marked as rollback-only异常,这个异常的意思时表示spring不建议用户应该让内层事务的异常直接最终被抛出而不是被上层事务所catch(假如我们有内层事务回滚而不影响外层事务提交的这种场景,应当去考虑使用NESTED这个事务传播级别)