资讯专栏INFORMATION COLUMN

Spring嵌套事务是怎么回滚的?

vboy1010 / 2834人阅读

摘要:希望当注册课程发生错误时,只回滚注册课程部分,保证用户信息依然正常。期待结果是即便内部事务发生异常,外部事务俘获该异常后,内部事务应自行回滚,不影响外部事务。综上外层事务是否回滚的关键,最终取决于,该方法返回值正是在内层异常时设置的。

  • 事务的传播机制
  • 多数据源的切换问题

更深入理解 Spring 事务。

用户注册完成后,需要给该用户登记一门PUA必修课,并更新该门课的登记用户数。
为此,我添加了两个表。
课程表 course,记录课程名称和注册的用户数。

用户选课表 user_course,记录用户表 user 和课程表 course 之间的多对多关联。

同时为课程表初始化了一条课程信息
接下来我们完成用户的相关操作,主要包括两部分:

  • 新增用户选课记录
  • 课程登记学生数 + 1

新增业务类 CourseService实现相关业务逻辑,分别调用了上述方法保存用户与课程的关联关系,并给课程注册人数+1

为避免注册课程的业务异常导致用户信息无法保存,这里 catch 注册课程方法中抛出的异常。希望当注册课程发生错误时,只回滚注册课程部分,保证用户信息依然正常。

为验证异常是否符合预期,在 regCourse() 里抛一个注册失败异常:
执行代码:
注册失败部分的异常符合预期,但是后面又多了一个这样的错误提示:Transaction rolled back because it has been marked as rollback-only

最后用户和选课的信息都被回滚了,显然这不符预期。
期待结果是即便内部事务regCourse()发生异常,外部事务saveStudent()俘获该异常后,内部事务应自行回滚,不影响外部事务。
这是什么原因造成的呢?

源码解析

伪代码梳理整个事务的结构:

整个业务包含2层事务:

  • 外层 saveUser() 的事务
  • 内层 regCourse() 事务

Spring声明式事务中的propagation属性,表示对这些方法使用怎样的事务,即:
一个带事务的方法调用了另一个带事务的方法,被调用的方法它怎么处理自己事务和调用方法事务之间的关系。

propagation 有7种配置:

  • REQUIRED
    默认值,如果本来有事务,则加入该事务,如果没有事务,则创建新的事务。
  • SUPPORTS
  • MANDATORY
  • REQUIRES_NEW
  • NOT_SUPPORTED
  • NEVER
  • NESTED

因为:

  • 在 saveUser() 上声明了一个外部的事务,就已经存在一个事务了
  • 在propagation值为默认REQUIRED时

regCourse() 就会加入到已有的事务中,两个方法共用一个事务。

Spring 事务处理的核心:

TransactionAspectSupport.invokeWithinTransaction()

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,      final InvocationCallback invocation) throws Throwable {    TransactionAttributeSource tas = getTransactionAttributeSource();   final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);   final PlatformTransactionManager tm = determineTransactionManager(txAttr);   final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);   if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {      // 是否需要创建一个事务      TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);      Object retVal = null;      try {         // 调用具体的业务方法         retVal = invocation.proceedWithInvocation();      }      catch (Throwable ex) {         // 当发生异常时进行处理         completeTransactionAfterThrowing(txInfo, ex);         throw ex;      }      finally {         cleanupTransactionInfo(txInfo);      }      // 正常返回时提交事务      commitTransactionAfterReturning(txInfo);      return retVal;   }   //......省略非关键代码.....}

整个方法完成了事务的一整套处理逻辑,如下:

  • 检查是否需要创建事务
  • 调用具体的业务方法进行处理
  • 提交事务
  • 处理异常

当前案例是两个事务嵌套,外层事务 saveUser()和内层事务 regCourse(),每个事务都会调用到这个方法。所以,该方法会被调两次。

内层事务

当捕获了异常,会调用

TransactionAspectSupport.completeTransactionAfterThrowing()

进行异常处理:

对异常类型做了一些检查,当符合声明中的定义后,执行具体的 rollback 操作,这个操作是通过如下方法完成:

AbstractPlatformTransactionManager

rollback()

该回滚实现负责处理正参与到已有事务集的事务。委托执行Rollback和doSetRollbackOnly。

继续调用

processRollback()


该方法里区分了三种场景:

  • 是否有保存点
  • 是否为一个新的事务
  • 是否处于一个更大的事务中

因为默认传播类型REQUIRED,嵌套的事务并未开启一个新事务,所以属于当前事务处于一个更大事务中,所以会走到分支1。

如下的判断条件确定是否设置为仅回滚:

if (status.isLocalRollbackOnly() ||	 isGlobalRollbackOnParticipationFailure())

满足任一,都会执行 doSetRollbackOnly():

  • isLocalRollbackOnly

    默认 false,当前场景为 false
  • isGlobalRollbackOnParticipationFailure()

    所以,就只由该方法来确定了,默认值为 true, 即是否回滚交由外层事务统一决定

条件得到满足,执行

DataSourceTransactionManager#doSetRollbackOnly

最终调用

DataSourceTransactionObject#setRollbackOnly()


内层事务操作执行完毕。

外层事务

外层事务中,业务代码就捕获了内层所抛异常,所以该异常不会继续往上抛,最后的事务会在 TransactionAspectSupport.invokeWithinTransaction() 中的

TransactionAspectSupport#commitTransactionAfterReturning()


该方法里执行了commit 操作:

AbstractPlatformTransactionManager#commit

当满足 !shouldCommitOnGlobalRollbackOnly() &&defStatus.isGlobalRollbackOnly(),就会回滚,否则继续提交事务:

  • shouldCommitOnGlobalRollbackOnly()
    若发现事务被标记了全局回滚,且在发生全局回滚时,判断是否应该提交事务,这个方法的默认返回 false,这里无需关注
  • isGlobalRollbackOnly()

    该方法最终进入

DataSourceTransactionObject#isRollbackOnly()

之前内部事务处理最终调用到DataSourceTransactionObject#setRollbackOnly()

public void setRollbackOnly() {   getConnectionHolder().setRollbackOnly();}
  • isRollbackOnly()
  • setRollbackOnly()

两个方法本质都是对ConnectionHolder.rollbackOnly属性标志位的存取
但ConnectionHolder则存在于DefaultTransactionStatus#transaction属性。

综上:外层事务是否回滚的关键,最终取决于DataSourceTransactionObject#isRollbackOnly(),该方法返回值正是在内层异常时设置的。
所以最终外层事务也被回滚,从而在控制台中打印上述日志。

这就明白了,Spring默认事务传播属性为REQUIRED:若已有事务,则加入该事务,若无事务,则创建新事务,因而内外两层事务都处于同一事务。
在 regCourse()中抛异常,并触发回滚操作时,这个回滚会继续传播,从而把 saveUser() 也回滚,最终整个事务都被回滚!

修正

Spring事务默认传播属性 REQUIRED,在整个事务的调用链上,任一环节抛异常都会导致全局回滚。

所以只需将传播属性改成 REQUIRES_NEW

运行:

异常正常抛出,注册课程部分的数据没有保存,但用户还是正常注册成功。这意味着此时Spring 只对注册课程这部分的数据进行了回滚,并没有传播到外层:

  • 当子事务声明为 Propagation.REQUIRES_NEW 时,在 TransactionAspectSupport.invokeWithinTransaction() 中调用 createTransactionIfNecessary() 就会创建一个新的事务,独立于外层事务
  • 而在 AbstractPlatformTransactionManager.processRollback() 进行 rollback 处理时,因为 status.isNewTransaction() 会因为它处于一个新的事务中而返回 true,所以它走入到了另一个分支,执行了 doRollback() 操作,让这个子事务多带带回滚,不会影响到主事务。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/125006.html

相关文章

  • 数据库 - 事务管理(ACID)隔离级别 事务传播行为

    摘要:关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。这种现象也是正常的,是由于事务的隔离级造成的,但是在在某些特别的情况下也是不允许的。指定业务方法绝对不能在事务范围内执行。内部事务的回滚不会对外部事务造成影响。 总览:showImg(https://segmentfault.com/img/bV3dRF?w=677&h=676); 事务的4大特性(ACID) 原子性(Atomic...

    lansheng228 评论0 收藏0
  • Spring中的事务控制

    摘要:中的事务控制方式编程式事务管理通过手动编码控制事务的边界,可以实现细粒度的事务控制,一般用的较少。隔离级别控制并发访问下数据库的安全性。内部事务的回滚不会对外部事务造成影响。可能导致脏幻不可重复读允许在并发事务已经提交后读取。 1.事务的概念 事务是一组操作的执行单元,相对于数据库的单条操作而言,事务管理的是一组SQL指令,如增删改查等,事务的特性体现在事务内包含的SQL指令必须全部执...

    Vixb 评论0 收藏0
  • spring事务增强,事务回滚如何判断?希望在前端上有个提示

    摘要:中人为的创建出一个异常,测试回滚在中可以被判断。手工抛出,作用有两个,使用事务增强,对事务回滚。中可以捕获该异常,并处理,例在前端显示提示信息。例子结果说明事务增强可以通过进行事务增强。事务发生回滚时,可以判断事务发生了回滚,并处理。 1 主要处理思路 1.1 思路1 事物回滚,一般抛异常,可以自己手写一个异常,根据异常判断。事物还是按照 spring 的之前的逻辑。只是,手工抛特定的...

    geekidentity 评论0 收藏0
  • Spring知识——事务解析

    摘要:编程式事务指的是通过编码方式实现事务声明式事务基于将具体业务逻辑与事务处理解耦。声明式事务管理使业务代码逻辑不受污染因此在实际使用中声明式事务用的比较多。声明式事务有两种方式,一种是在配置文件中做相关的事务规则声明,另一种是基于注解的方式。 事务管理是应用系统开发中必不可少的一部分。Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编码式和声明式的两种方式。编程式...

    tuomao 评论0 收藏0
  • spring事务处理

    摘要:声明式事务管理的事务管理是通过代理实现的。其中的事务通知由元数据目前基于或注解驱动。代理对象与事务元数据结合产生了一个代理,它使用一个实现品配合,在方法调用前后实施事务。 JDBC事务 1.获取连接 Connection con=DriverManager.getConnection(); 2.开启事务 con.setAutoCommit(true/fase); 3.执行CRUD 4....

    李文鹏 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<