Spring事务传播方式 REQUIRED 与 NESTED 踩坑

产生疑惑

开始对REQUIRED 跟 NESTED 事务传播方式不是很理解,网上大多数材料介绍的诸如:

1、嵌套事务有回滚点,如果抛出异常嵌套事务会回滚到回滚点

2、嵌套事务如果外层没有事务会新开一个事务

3、嵌套事务会比外层事务先提交

。。。

剩下就是写利用嵌套事务的case

当时就感觉这不是跟 REQUIRED 一样吗,好像也没啥区别,代码中有一块这样的逻辑用到了嵌套事务,去找TL问它也不是很清楚这块当时为啥要这么写,代码如下:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.NESTED)public void insertProvideInfo(TblLoanProvideInfo tblLoanProvideInfo) {tblLoanProvideInfoDao.insert(tblLoanProvideInfo);TblMappingInfo tblMappingInfo = buildMappingInfo(tblLoanProvideInfo);tblMappingUserInfo.insert(tblMappingInfo);}

单看这段代码好像REQUIRED就能满足,当时在做流程重构,于是把事务传播改成了REQUIRED

然后自测了下没问题,让QA跑了遍自动化任务没问题就直接发了。

问题产生

上周五,我们有一批订单出了问题,修改订单信息后找业务来同步,但是业务同步后的状态与我们库中的状态不一致,并不是某几笔单子有问题,而是所有单子同步都有问题,于是开始排查问题

查日志之后发现报了一个这样的异常

日志信息告诉我当前事务已经回滚了

看到这里并没有发现问题所在,于是去看下抛异常的代码逻辑

@Transactional(rollbackFor = RuntimeException.class)public ProvideRespDto insertAndCheckRequest(ProvideReqDto provideReqDto, TblLoanProvideInfo tblLoanProvideInfo) {try {//调用上面代码的插入逻辑provideInfoServiceProxy.insert(tblLoanProvideInfo);} catch (DuplicateKeyException e) {//如果发生唯一键冲突,则为业务贷款流水重复,即业务原单重发,返回数据库中的结果TblLoanProvideInfo dbTblLoanProvideInfo = provideInfoServiceProxy.queryByLoanProvideIdForUpdate(tblLoanProvideInfo.getLoanProvideId());return convertProvideRespFromDb(dbTblLoanProvideInfo);}return null;}

业务同步单子的逻辑很简单,把原单重复发起,我们这边做下幂等返回库中的数据就好,其实这里已经差不多能看出原因了,处理逻辑中套了两层事务,我们知道@Transaction这个注解是基于切面实现的,类似于下面的代码

public void testTransactional(){DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();defaultTransactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);TransactionStatus transaction = transactionManager.getTransaction(defaultTransactionDefinition);try{//do something} catch (Exception e) {transactionManager.rollback(transaction);throw e;}transactionManager.commit(transaction);}

最外面一层采用的默认REQUIRED传播方式,内层插入也采用这种插入方式,并且内外层是一个事务,当内层事务抛出异常的时候会回滚整个事务,所以下面查询库中信息就有问题了,事务已经回滚了,因此抛出上述异常。

NESTED事务跟REQUIRED事务区别就在这里,NESTED事务是回滚到回滚点,而回滚点生成是在进入内嵌事务的时候,外面事务是不会回滚的

具体创建回滚点代码是在这里

if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {if (!isNestedTransactionAllowed()) {throw new NestedTransactionNotSupportedException("Transaction manager does not allow nested transactions by default - " +"specify 'nestedTransactionAllowed' property with value 'true'");}if (debugEnabled) {logger.debug("Creating nested transaction with name [" + definition.getName() + "]");}if (useSavepointForNestedTransaction()) {// Create savepoint within existing Spring-managed transaction,// through the SavepointManager API implemented by TransactionStatus.// Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);status.createAndHoldSavepoint();return status;}else {// Nested transaction through nested begin and commit/rollback calls.// Usually only for JTA: Spring synchronization might get activated here// in case of a pre-existing JTA transaction.boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, null);doBegin(transaction, definition);prepareSynchronization(status, definition);return status;}
}

处理回滚点是在processRollback中

                if (status.hasSavepoint()) {if (status.isDebug()) {logger.debug("Rolling back transaction to savepoint");}status.rollbackToHeldSavepoint();}else if (status.isNewTransaction()) {if (status.isDebug()) {logger.debug("Initiating transaction rollback");}doRollback(status);}
@Overridepublic void rollbackToSavepoint(Object savepoint) throws TransactionException {ConnectionHolder conHolder = getConnectionHolderForSavepoint();try {conHolder.getConnection().rollback((Savepoint) savepoint);}catch (Throwable ex) {throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex);}}
@Overrideprotected void doRollback(DefaultTransactionStatus status) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();Connection con = txObject.getConnectionHolder().getConnection();if (status.isDebug()) {logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");}try {con.rollback();}catch (SQLException ex) {throw new TransactionSystemException("Could not roll back JDBC transaction", ex);}}

可以看到这两个回滚的处理不一样,一个回滚到创建回滚点的地方,另一个回滚整个事务

解决方案

其实解决方案已经很明了了,将事务传播方式改回NESTED就ok了

心得体会

其实还是自己对这两个传播方式理解不深刻,测试覆盖场景也不是很全面,导致出现这种问题,今后改动历史代码时一定要小心,尤其是对自己并不很熟悉的代码,一定要在三推敲为什么要这样写,很可能中间包含着一种业务场景。

 


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部