关于spring:用户自定义的-trycache-是否会影响-Spring-Transactional-嵌套事务方法的执行结果

5次阅读

共计 8522 个字符,预计需要花费 22 分钟才能阅读完成。

毋庸置疑,答案是必定的。然而 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 事务切面 ,那整个事务会被 失常提交

正文完
 0