用户自定义的|用户自定义的 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); } } }

【用户自定义的|用户自定义的 try-cache 是否会影响 Spring @Transactional 嵌套事务方法的执行结果 ()】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 事务切面,那整个事务会被正常提交。

    推荐阅读