共计 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 事务切面进行提交时,发现以后所持有的 ConnectionHolder
的 rollbackOnly
属性值为 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 将以后 DataSource
和 ConnectionHolder
的一对一绑定关系保留在了 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 事务切面 ,那整个事务会被 失常提交。