用户自定义的 try-cache 是否会影响 Spring @Transactional 嵌套事务方法的执行结果 ?

毋庸置疑,答案是肯定的。但是 try-cache 的不同位置究竟是如何影响 Spring 事务切面的运行结果呢?别急,接下来笔者会慢慢道来 ~

本文示例代码均基于 @Transactional(propagation = Propagation.REQUIRED)

在外层事务方法中使用 try-cache 捕获自定义异常

首先给出本文中第一段示例代码:

ServiceC.java

@Service
public class ServiceC {

  private final Logger logger = LogManager.getLogger(ServiceC.class);

  private final ServiceA serviceA;
  private final ServiceB serviceB;

  @Autowired
  public ServiceC(ServiceA serviceA, ServiceB serviceB) {
    this.serviceA = serviceA;
    this.serviceB = serviceB;
  }

  @Transactional
  public void doSomethingOneForC() throws SQLException {
    try {
      logger.info("====== using {} doSomethingForC ======", this.serviceA);
      this.serviceA.doSomethingOneForA();
      logger.info("====== using {} doSomethingForC ======", this.serviceB);
      this.serviceB.doSomethingOneForB();
    } catch (RuntimeException e) {
      logger.warn("cached runtime exception", e);
    }
  }
}

ServiceA.java

@Service
public class ServiceA {

  private final Logger logger = LogManager.getLogger(ServiceA.class);

  private final DataSource dataSource;

  @Autowired
  public ServiceA(DataSource dataSource) {
    this.dataSource = dataSource;
  }

  @Transactional
  public void doSomethingOneForA() throws SQLException {
    logger.info("Start inserting record into tableA, current dataSource: {}", this.dataSource);
    Connection connection = DataSourceUtils.getConnection(dataSource);
    if (connection.getAutoCommit()) {
      connection.setAutoCommit(false);
    }
    String insertQuery = "INSERT INTO tablea (id, name) VALUES (?, ?)";
    PreparedStatement preparedStatement = connection.prepareStatement(insertQuery);
    preparedStatement.setInt(1, 1);
    preparedStatement.setString(2, "Iphone SE");
    int i = preparedStatement.executeUpdate();
  }
}

ServiceB.java

@Service
public class ServiceB {

  private final Logger logger = LogManager.getLogger(ServiceB.class);

  private final DataSource dataSource;

  @Autowired
  public ServiceB(DataSource dataSource) {
    this.dataSource = dataSource;
  }

  @Transactional
  public void doSomethingOneForB() throws SQLException {
    logger.info("Start inserting record into tableB, current dataSource: {}", this.dataSource);
    Connection connection = DataSourceUtils.getConnection(dataSource);
    if (connection.getAutoCommit()) {
      connection.setAutoCommit(false);
    }
    String insertQuery = "INSERT INTO tableb (id, name) VALUES (?, ?)";
    PreparedStatement preparedStatement = connection.prepareStatement(insertQuery);
    preparedStatement.setInt(1, 1);
    preparedStatement.setString(2, "Alvin");
    int i = preparedStatement.executeUpdate();
    throw new RuntimeException("manual error occurs");
  }
}

Test.java

@Test
void test13() throws SQLException {
  AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BASE_PACKAGE);
  ServiceC beanC = applicationContext.getBean(ServiceC.class);
  beanC.doSomethingOneForC();
}

在以上代码示例中,外层 ServiceC 的事务方法和内层 ServiceA, ServiceB 的事务方法采用的事务传播属性均为 Propagation.REQUIRED,即三个事务方法处于同一个事务中。在内层 ServiceB 事务方法中手动抛出一个运行时异常,外层 ServiceC 事务方法中捕获 RuntimeException执行结果是:两张表中都未插入成功
笔者相信,这个执行结果可能有点出乎意料:外层捕获异常之后,不是应该正常提交,两张表分别写入一条数据么?接下来让我们从源码层面分析为什么 Spring 会这么处理

首先为了加深理解,笔者用伪代码给出 Spring 嵌套事务的流程(具体细节,详见 org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction):

// ServiceC 事务切面
try {

  // ServiceA 事务切面
  try {
    // 执行 ServiceA 事务方法,插入一条数据到 tablea
  } cache (Throwable ex) {
    completeTransactionAfterThrowing(txInfo, ex);
    throw ex;
  } finally {
    cleanupTransactionInfo(txInfo);
  }
  ...
  commitTransactionAfterReturning(txInfo);

  // ServiceB 事务切面
  try {
    // 执行 ServiceB 事务方法,插入一条数据到 tableb
    // 手动 throw 一个 RuntimeException
  } cache (Throwable ex) {
    completeTransactionAfterThrowing(txInfo, ex);
    throw ex;
  } finally {
    cleanupTransactionInfo(txInfo);
  }
  ...
  commitTransactionAfterReturning(txInfo);

} cache (Throwable ex) {
  completeTransactionAfterThrowing(txInfo, ex);
  throw ex;
} finally {
  cleanupTransactionInfo(txInfo);
}
...
commitTransactionAfterReturning(txInfo);

从伪代码中,我们可以看到内层 ServiceB 的事务切面捕获了我们手动抛出的异常,那按理来说外层 ServiceC 的事务切面确实应该正常提交(至于为什么内层 ServiceA, ServiceB 的事务切面提交不生效,是因为Spring 规定了只有新创建的事务才会真正进行提交,而本例中内层 ServiceA 和 ServiceB 所使用的事务都是 Service 创建的事务,所以内层事务切面处理完成之后并不会进行提交。具体细节读者可以自行查看org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit)。玄机就在 org.springframework.jdbc.datasource.DataSourceTransactionManager.DataSourceTransactionObject#setRollbackOnly。在 ServiceB 的事务切面捕获异常,进行回滚操作时,发现当前事务不是在当前事务切面中新创建的事务,所以将当前所持有的 ConnectionHolder 中的 rollbackOnly 属性设置成了 true。而 ConnectionHolder 和线程ID是一一绑定的。在外层 ServiceC 事务切面进行提交时,发现当前所持有的 ConnectionHolderrollbackOnly 属性值为 true,所以将整个事务进行了回滚,因此我们得到的结果是 tablea 和 tableb 都一条数据都没有 insert 成功。
以下是 ServiceB 事务切面设置该属性,以及 ServiceC 事务切面最终进行全局回滚的细节

// org.springframework.jdbc.datasource.DataSourceTransactionManager#doSetRollbackOnly
@Override
protected void doSetRollbackOnly(DefaultTransactionStatus status) {
  DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
  if (status.isDebug()) {
    logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + "] rollback-only");
  }
  txObject.setRollbackOnly();
}

// org.springframework.transaction.support.AbstractPlatformTransactionManager#commit
@Override
public final void commit(TransactionStatus status) throws TransactionException {
  ......
  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;
  }
  ......
}

// org.springframework.transaction.support.DefaultTransactionStatus#isGlobalRollbackOnly
@Override
public boolean isGlobalRollbackOnly() {
  return ((this.transaction instanceof SmartTransactionObject) && ((SmartTransactionObject) this.transaction).isRollbackOnly());
}

关于为什么 ServiceC, ServiceB, ServiceA 的事务切面持有的是同一个 ConnectionHolder,其实是在事务切面开始时,Spring 将当前 DataSourceConnectionHolder 的一对一绑定关系保存在了 ThreadLocal中,详见org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin)

...
// Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
  TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
...

通过上述代码片段,笔者想给各位读者传递一个信息:如果多个数据库操作处于同一个事务中,那么他们所持有的 connection 一定是同一个

在内层事务方法中使用 try-cache 捕获自定义异常

清除 tablea 和 tableb 中的测试数据,接下来给出第二段示例代码

ServiceA.java

@Service
public class ServiceA {

  private final Logger logger = LogManager.getLogger(ServiceA.class);

  private final DataSource dataSource;

  @Autowired
  public ServiceA(DataSource dataSource) {
    this.dataSource = dataSource;
  }

  @Transactional
  public void doSomethingOneForA() throws SQLException {
    logger.info("Start inserting record into tableA, current dataSource: {}", this.dataSource);
    Connection connection = DataSourceUtils.getConnection(dataSource);
    if (connection.getAutoCommit()) {
      connection.setAutoCommit(false);
    }
    String insertQuery = "INSERT INTO tablea (id, name) VALUES (?, ?)";
    PreparedStatement preparedStatement = connection.prepareStatement(insertQuery);
    preparedStatement.setInt(1, 1);
    preparedStatement.setString(2, "Iphone SE");
    int i = preparedStatement.executeUpdate();
  }
}

ServiceB.java

@Service
public class ServiceB {

  private final Logger logger = LogManager.getLogger(ServiceB.class);

  private final DataSource dataSource;

  @Autowired
  public ServiceB(DataSource dataSource) {
    this.dataSource = dataSource;
  }

  @Transactional
  public void doSomethingOneForB() throws SQLException {
    try {
      logger.info("Start inserting record into tableB, current dataSource: {}", this.dataSource);
      Connection connection = DataSourceUtils.getConnection(dataSource);
      if (connection.getAutoCommit()) {
        connection.setAutoCommit(false);
      }
      String insertQuery = "INSERT INTO tableb (id, name) VALUES (?, ?)";
      PreparedStatement preparedStatement = connection.prepareStatement(insertQuery);
      preparedStatement.setInt(1, 1);
      preparedStatement.setString(2, "Alvin");
      preparedStatement.executeUpdate();
      throw new RuntimeException("manual error occurs");
    } catch (RuntimeException e) {
      logger.warn("cached runtime exception", e);
    }
  }
}

ServiceC.java

@Service
public class ServiceC {

  private final Logger logger = LogManager.getLogger(ServiceC.class);

  private final ServiceA serviceA;
  private final ServiceB serviceB;

  @Autowired
  public ServiceC(ServiceA serviceA, ServiceB serviceB) {
    this.serviceA = serviceA;
    this.serviceB = serviceB;
  }

  @Transactional
  public void doSomethingOneForC() throws SQLException {
    logger.info("====== using {} doSomethingForC ======", this.serviceA);
    this.serviceA.doSomethingOneForA();
    logger.info("====== using {} doSomethingForC ======", this.serviceB);
    this.serviceB.doSomethingOneForB();
  }
}

Test.java

@Test
void test13() throws SQLException {
  AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BASE_PACKAGE);
  ServiceC beanC = applicationContext.getBean(ServiceC.class);
  beanC.doSomethingOneForC();
}

在上述示例代码中,ServiceB 事务方法中手动抛出运行时异常,然后被 try-cache 代码块捕获,外层 ServiceC 的事务方法没有 try-cache 代码块。执行结果是:tablea 和 tableb 分别插入一条数据
相信有了上面的铺垫,读者们可以很快想到为什么会有这样的结果: 异常被用户代码吞掉之后,ServiceB 的事务切面中的 try-cache 代码块并未捕获到任何异常,所以 Spring 认为 ServiceB 事务方法执行成功返回,进而外层 ServiceC 的事务切面处理结束之后,最终进行了事务的提交,所以会有数据插入成功的结果。

总结

通过以上讲解,我们可以得到这样一个结论:在 Propagation.REQUIRED 事务传播属性下,嵌套事务中只要被事务切面捕获到异常,那最终的执行结果是全部回滚;如果异常在发生的地方被用户自定义的 try-cache 捕获而并未抛给 Spring 事务切面,那整个事务会被正常提交

你可能感兴趣的:(用户自定义的 try-cache 是否会影响 Spring @Transactional 嵌套事务方法的执行结果 ?)