一次bug修复,重新认识spring事务传播机制—破案篇
❝前段时间,QA在jira提了个bug,偶现一个问题。报错:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
很遗憾,当时没有记录整个bug修复过程,现在那段代码也让我替换掉了。
不过在解决的过程中,让我对spring事务传播机制以及spring事务又多了一份了解。
当时出现这个没见过的异常应该是,类A的一个方法需要用事务处理,在调用B的时候,没有注意B的方法有事务,但是想对B的调用异常结果做额外业务处理,就加了try catch。
今天按照这个场景还原一下 ”案发现场“。
-
事务A
/**
* 事务A
* created by mayibz on 2021/1/15
*/
@Service
@Slf4j
public class TransactionA {
@Autowired
private TransactionB transactionB;
@Autowired
private TestTransactionMapper testTransactionMapper;
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void methodA() {
log.info("开始执行事务A");
testTransactionMapper.insertRecord("test record");
try {
transactionB.methodB();
}catch (Exception e) {
log.info("事务B执行异常,不打印异常栈");
}
log.info("事务A执行完毕");
}
}
-
事务B
/**
* 事务B
* created by mayibz on 2021/1/15
*/
@Service
@Slf4j
public class TransactionB {
@Transactional(rollbackFor = Exception.class)
public void methodB() {
log.info("开始执行事务B");
throw new RuntimeException("执行事务B异常");
}
}
-
从事务A开始调用
@Test
public void test() {
transactionA.methodA();
}
如果这样做,你觉得会出现什么情况呢?
如果只限理论学习,或者不够深入,很容易给出如下结论:
-
日志只会输出 :开始执行事务A, 开始执行事务B, 事务B执行异常,不打印异常栈,事务A执行完毕 -
事务A成功插入一条record
然而,事实并非如此:
那么就让我们带着问题,使用debug,福尔摩斯的华生!走进科学(源码)!!!
注解开启的事务,当然是通过切面,然后进入到spring 提供的 TransactionAspectSupport
类中。
首先执行事务A,即插入一条recod,然后调用事务B。
毫无疑问,事务B会抛出异常。
调用processRollback()
, 处理事务B的回滚。这是事务B的当前事务状态(这块已经出现了savepoint的身影,spring处理嵌套事务的关键)
然后根据当前状态值,在这一步,通过doSertRollbackOnly()
调用 数据库的rollback,进行事务B的回滚。
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();
}
记住这个方法,doSetRollbackOnly()
,案情突破的伏笔!
最后在这两步操作,抛出了当时出现的错误异常!
然后,事务B执行完毕,抛出我们代码中定义的异常
再被我们的try catch 代码捕获
然后到事务A,执行后续的代码
事务A后续代码执行完毕后,准备执行事务提交
shoulodCommitOnGlobalRollbackOnly()
默认是true,
所以也就是说 因为事务A的 isGlobalRollbackOnly()
返回的是ture,表示事务被标记了全局回滚,
导致了事务A也进行了回滚。
追踪isGlobalRollbackOnly()这个方法,来到了 ResourceHoderSupport
的rollbackOnly
属性。
然后又一路找set这个字段的方法调用,最终发现就是我们事务B在执行processRollback()
方法的时候,也就是上面提到的伏笔,将事务标记了全局回滚!!!
也正是因为事务B标记了全局回滚,
才会抛出 UnexpectedRollbackException
至此,结案。撒花~~~
结案报告:
事务A先执行插入数据库一条记录操作,然后调用事务B,事务B发生异常。
spring在按要求对事务B回滚时,除了对事务B进行回滚外,还设置了全局回滚点为true, 导致抛出UnexpectedRollbackException
。
虽然A中捕获了B的异常,但是事务A在提交时,发现全局回滚点为true,也进行了回滚操作。
ps:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
这个异常最终是由A事务抛出的,而不是B事务。
B事务在处理回滚时最后一步抛出的是当时本代码定义的异常。
并且在事务A中捕获的异常级别是Exception
级别, 我们在case中并未打印异常栈。
回过头来再想,我们如果事务B为默认传播机制,即REQUIRED
,表明假如在父事务A中嵌套执行,应该同事务A作为一个整体事务来执行,所以B回滚,A回滚; A回滚,B回滚。 这也是spring期望正确使用这个传播机制的姿势。
即使期望事务B的执行不干扰事务A的commit,那也不应该用 try catch这种方式去处理,那应该如何处理呢? 敬请期待下一篇《原理篇》!