毋庸置疑,答案是肯定的。但是 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 {
// 开启事务
createTransactionIfNecessary()
// ServiceA 事务切面
try {
// 开启事务
createTransactionIfNecessary()
// 执行 ServiceA 事务方法,插入一条数据到 tablea
} cache (Throwable ex) {
// 回滚事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
...
// 提交事务
commitTransactionAfterReturning(txInfo);
// ServiceB 事务切面
try {
// 开启事务
createTransactionIfNecessary()
// 执行 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 事务切面,那整个事务会被正常提交。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。