likes
comments
collection
share

Spring事务默认传播行为PROPAGATION_REQUIRED:怎样做到异常被捕获处理后事务还会回滚

作者站长头像
站长
· 阅读数 17

Spring事务默认传播行为PROPAGATION_REQUIRED:怎样做到异常被捕获处理后事务还会回滚

前言

PROPAGATION_REQUIRED是Spring默认的事务传播机制,如果当前没有事务,就新建一个事务,如果当前已经存在一个事务,加入到当前事务。

话不多说,直接上代码。

场景代码:在方法testinsert(User user)insert_Exception(User user)方法上都添加了@Transactional,事务传播行为都是默认的PROPAGATION_REQUIRED

测试方法中事务默认会自动回滚,可以在测试方法上加@Rollback(value = false)修改配置

@Transactional
@Rollback(value = false)
@Test
public void test(){
    ...
    // 插入一条记录,正常执行
    userService.insert(user1);
  	...
    try {
        // 插入一条记录,执行过程中抛出一个RuntimeException
        userService.insert_Exception(user2);
    } catch (Exception e) {
        log.error("异常信息: {}", e.getMessage());
    }
}

public class UserService {
    ...
    @Transactional
    void insert(User user) {
        userMapper.insert(user);
    }
   
    @Transactional
    void insert_Exception(User user) {
        userMapper.insert(user);
        throw new RuntimeException("抛出异常");
    }
}

test执行结果:事务回滚,插入记录均不成功。

相信结论大家早已知晓。

userService.insert(user1)执行正常,userService.insert_Exception(user2)执行过程中抛出异常,异常最终在test方法中被捕获,test方法执行正常,那么事务是怎么知道要回滚的?

解析

我们已经在测试方法上加了@Transactional注解,测试方法会被配置为在事务中运行,所以会执行startTransaction

有一个属性:flaggedForRollbackflaggedForRollback是事务的回滚标志,根据这个回滚标志,会立即强制提交或回滚配置测试上下文的事务。

// 为配置的测试上下文启动一个新的事务
void startTransaction() {
   ...
   // 已经加了@Rollback(value = false),事务不会自动回滚,值为false
   this.flaggedForRollback = this.defaultRollback;
   // 通过事务管理器获取当前存在的事务状态
   this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition);
   ...
}

通过事务管理器获取当前的事务状态transactionStatus,首先创建一个事务对象,通过TransactionSynchronizationManager获取connectionHolder,由于当前不存在事务,所以不存在connectionHolder

connectionHolder:包装JDBC连接的资源持有者,而spring的事务是通过数据库连接来实现的,所以开启一个新事务时会创建一个数据源对象来表示ConnectionHolder,作为DataSourceTransactionManager的事务对象使用。

// 根据事务对象中的connectionHolder来判断当前是否存在事务
protected boolean isExistingTransaction(Object transaction) {
   DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
   // 事务对象中connectionHolder为空,返回false
   return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive());
}

因为当前不存在事务,isExistingTransaction(Object transaction)执行结果返回false,且要开启的事务传播行为是PROPAGATION_REQUIRED,最后会执行startTransaction方法,开启新事务,标记当前事务状态transactionStatusnewTransactionnewConnectionHolder为true,表示当前是一个新事务,同时也会创建一个connectionHolder对象,该对象中包含了数据源的连接。

Spring事务默认传播行为PROPAGATION_REQUIRED:怎样做到异常被捕获处理后事务还会回滚

事务开启后,代码执行到userService.insert(user1),首先获取当前的事务状态,执行doGetTransaction()获取当前事务状态的事务对象,由于当前已经存在事务,当前事务状态的事务对象中connectionHolder不为空,执行isExistingTransaction(Object transaction)返回true,进入handleExistingTransaction(def, transaction, debugEnabled)方法,且userService.insert(user1)的事务传播行为是PROPAGATION_REQUIRED,此时不会开启新事务,只是创建一个TransactionStatus来表示其当前的事务状态,其中newTransactionnewConnectionHolder为false,事务状态中的connectionHolder与开启事务时创建的connectionHolder是同一个对象

Spring事务默认传播行为PROPAGATION_REQUIRED:怎样做到异常被捕获处理后事务还会回滚 userService.insert(user1)正常执行,然后进入commitTransactionAfterReturning(txInfo)

/**
 * 在成功完成调用后执行
 * txInfo -关于当前事务的信息
 */
protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
   if (txInfo != null && txInfo.getTransactionStatus() != null) {
      ...
      txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
   }
}

commit方法中,userService.insert(user1)已经加入外围方法的事务,根据其事务状态会执行processCommit(defStatus)方法,这个方法会处理实际提交的事务,这里事务没有实际提交,所以并没有执行任何操作。

以下是执行commit对事务状态的一些判断

// status 当前事务状态
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;
   // 如果回滚标志已经设置为true,则开始回滚
   if (defStatus.isLocalRollbackOnly()) {
      ...
      processRollback(defStatus, false);
      return;
   }
   // shouldCommitOnGlobalRollbackOnly():返回是否以全局方式对已标记为仅回滚的事务调用doCommit,默认值:false。如果	    // rollbackOnly标记被设置为true,那么事务就会回滚而不会提交
   // defStatus.isGlobalRollbackOnly(),判断rollbackOnly的值
   if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
      ...
      }
      // 处理回滚操作
      processRollback(defStatus, true);
      return;
   }
   // 提交事务
   processCommit(defStatus);
}

继续执行到userService.insert_Exception(user2),这里也会加入外围方法的事务,创建一个事务状态对象,操作和userService.insert(user1)一样,但是在方法执行过程中抛出异常,这里不会执行commitTransactionAfterReturning,变成执行completeTransactionAfterThrowing(txInfo, ex)

开始处理回滚操作,执行doSetRollbackOnly方法,将ConnectionHolder的仅回滚标记rollbackOnly设置为为true,最后userService.insert_Exception(user2)抛出异常到外围方法,异常在外围方法中被捕获并处理。

Spring事务默认传播行为PROPAGATION_REQUIRED:怎样做到异常被捕获处理后事务还会回滚

这时因为已经处理了userService.insert_Exception(user2)抛出的异常,测试方法test是正常执行完成的,而且之前添加了@Rollback(value = false),将flaggedForRollback设置为false,事务并不会自动回滚,所以事务开始执行提交操作,但是在处理事务提交之前,会根据事务状态(status中的rollbackOnly)和全局事务的仅回滚标记(connectionHolder中的rollbackOnly)分别进行判断是否要进行回滚操作。

如果this.flaggedForRollback的值为true,那么在endTransaction()会直接执行rollback方法,不会执行commit方法,在rollback方法中会执行processRollback(defStatus, false),然后完成事务回滚。

// 根据回滚标志,立即强制提交或回滚配置测试上下文的事务
void endTransaction() {
   ...
   try {
      // 由于之前添加了@Rollback(value = false),将flaggedForRollback设置为false,这里会执行提交方法
      if (this.flaggedForRollback) {
         this.transactionManager.rollback(this.transactionStatus);
      }
      else {
         this.transactionManager.commit(this.transactionStatus);
      }
   }
   ...
}
// status 当前事务状态
public final void commit(TransactionStatus status) throws TransactionException {
   ...
   DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
   // 通过事务状态的仅回滚标志来判断是否回滚
   if (defStatus.isLocalRollbackOnly()) {
      ...
      // 处理回滚操作,参数unexpected为false
      processRollback(defStatus, false);
      return;
   }
   // 通过判断全局事务本身被事务协调器是否被标记为回滚
   if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {
      ...通过检查事务对象确定仅回滚标志  
      // 处理回滚操作,参数unexpected为true
      processRollback(defStatus, true);
      return;
   }
   // 提交事务
   processCommit(defStatus);
}

在上面的代码中,有两处代码会判断仅回滚标志rollbackOnly,但是最终都会执行processRollback来回滚事务,那么processRollback(defStatus, false)processRollback(defStatus, true)会执行哪个方法,为什么它们的第二个参数(参数名:unexpected)又有什么不同呢?

首先解释一下为什么仅回滚标志rollbackOnly能决定现有事务的回滚

在事务管理器中,有一个属性globalRollbackOnParticipationFailure,默认值为true,用来设置是否在参与的事务失败后将现有事务全局标记为仅回滚。而在shouldCommitOnGlobalRollbackOnly()方法中,决定了是否以全局方式对已标记为仅回滚的事务调用doCommit,也就是执行提交操作,其默认值为false。

因此,在这种情况下(设置了仅回滚标志为true),事务管理器将触发回滚,然后抛出一个unexpectedRollbackexception

那么以上代码会执行哪个方法?

defStatus指的是test方法的事务状态,defStatus.isLocalRollbackOnly()检查的是defStatus的仅回滚标记rollbackOnly,test方法正常执行成功,事务状态中的仅回滚标记为false。

想一想在方法执行失败时是怎么设置仅回滚标记rollbackOnly的,没错,就是在connectionHolder,所以检查全局事务是否被标记为回滚就是检查connectionHolder中的仅回滚标记rollbackOnly

// defStatus.isGlobalRollbackOnly()的实现
@Override
public boolean isRollbackOnly() {
	return getConnectionHolder().isRollbackOnly();
}

为什么processRollback处理回滚操作,参数unexpected一个为true,一个为false?

unexpected,就是意外的意思。

  • processRollback(defStatus, true):test方法正常执行完成,本来要执行提交操作,但是全局事务本身被事务协调器被标记为回滚,所以变为执行回滚操作,unexpected为true表示这是意外回滚。简而言之,本来要提交,最后回滚了,这就是意外回滚,所以最后也会抛出意外回滚异常。
  • processRollback(defStatus, false):方法执行过程中抛有异常未处理导致事务回滚时,unexpected为false,unexpected为为false,表示该回滚并不意外。

总结

一言以蔽之:在事务传播行为都是PROPAGATION_REQUIRED的情况下,如果方法开启了新事务,首先会获取当前事务状态,没有connectionHolder就会创建一个,如果方法内部调用事务方法,被调用的方法都会加入当前事务,共用同一个connectionHolder,如果被调用的方法出现异常而导致回滚,就会设置connectionHolder中的仅回滚标记rollbackOnly为真,就是标记全局事务为仅回滚,这样就算在外围方法中处理了被调用方法抛出的异常,最终事务还是会执行回滚操作。

OK,就这样吧,有错误的地方可以指出,欢迎讨论!