草庐IT

#yyds干货盘点# Spring嵌套事务是怎么回滚的?

公众号JavaEdge 2023-03-28 原文
用户注册完成后,需要给该用户登记一门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() 操作,让这个子事务单独回滚,不会影响到主事务。


有关#yyds干货盘点# Spring嵌套事务是怎么回滚的?的更多相关文章

  1. ruby-on-rails - Rails 编辑表单不显示嵌套项 - 2

    我得到了一个包含嵌套链接的表单。编辑时链接字段为空的问题。这是我的表格:Editingkategori{:action=>'update',:id=>@konkurrancer.id})do|f|%>'Trackingurl',:style=>'width:500;'%>'Editkonkurrence'%>|我的konkurrencer模型:has_one:link我的链接模型:classLink我的konkurrancer编辑操作:defedit@konkurrancer=Konkurrancer.find(params[:id])@konkurrancer.link_attrib

  2. ruby - 将散列转换为嵌套散列 - 2

    这道题是thisquestion的逆题.给定一个散列,每个键都有一个数组,例如{[:a,:b,:c]=>1,[:a,:b,:d]=>2,[:a,:e]=>3,[:f]=>4,}将其转换为嵌套哈希的最佳方法是什么{:a=>{:b=>{:c=>1,:d=>2},:e=>3,},:f=>4,} 最佳答案 这是一个迭代的解决方案,递归的解决方案留给读者作为练习:defconvert(h={})ret={}h.eachdo|k,v|node=retk[0..-2].each{|x|node[x]||={};node=node[x]}node[

  3. ruby - 我可以使用 aws-sdk-ruby 在 AWS S3 上使用事务性文件删除/上传吗? - 2

    我发现ActiveRecord::Base.transaction在复杂方法中非常有效。我想知道是否可以在如下事务中从AWSS3上传/删除文件:S3Object.transactiondo#writeintofiles#raiseanexceptionend引发异常后,每个操作都应在S3上回滚。S3Object这可能吗?? 最佳答案 虽然S3API具有批量删除功能,但它不支持事务,因为每个删除操作都可以独立于其他操作成功/失败。该API不提供任何批量上传功能(通过PUT或POST),因此每个上传操作都是通过一个独立的API调用完成的

  4. ruby-on-rails - 带 Spring 锁的 Rails 4 控制台 - 2

    我正在使用Ruby2.1.1和Rails4.1.0.rc1。当执行railsc时,它被锁定了。使用Ctrl-C停止,我得到以下错误日志:~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`gets':Interruptfrom~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.2/lib/spring/client/run.rb:47:in`verify_server_version'from~/.rvm/gems/ruby-2.1.1/gems/spring-1.1.

  5. Ruby——嵌套类和子类是一回事吗? - 2

    下面例子中的Nested和Child有什么区别?是否只是同一事物的不同语法?classParentclassNested...endendclassChild 最佳答案 不,它们是不同的。嵌套:Computer之外的“Processor”类只能作为Computer::Processor访问。嵌套为内部类(namespace)提供上下文。对于ruby​​解释器Computer和Computer::Processor只是两个独立的类。classComputerclassProcessor#Tocreateanobjectforthisc

  6. ruby - 模块嵌套代码风格偏好 - 2

    我的假设是moduleAmoduleBendend和moduleA::Bend是一样的。我能够从thisblog找到解决方案,thisSOthread和andthisSOthread.为什么以及什么时候应该更喜欢紧凑语法A::B而不是另一个,因为它显然有一个缺点?我有一种直觉,它可能与性能有关,因为在更多命名空间中查找常量需要更多计算。但是我无法通过对普通类进行基准测试来验证这一点。 最佳答案 这两种写作方法经常被混淆。首先要说的是,据我所知,没有可衡量的性能差异。(在下面的书面示例中不断查找)最明显的区别,可能也是最著名的,是你的

  7. ruby - Ruby 中的隐式返回值是怎么回事? - 2

    所以我开始关注ruby​​,很多东西看起来不错,但我对隐式return语句很反感。我理解默认情况下让所有内容返回self或nil但不是语句的最后一个值。对我来说,它看起来非常脆弱(尤其是)如果你正在使用一个不打算返回某些东西的方法(尤其是一个改变状态/破坏性方法的函数!),其他人可能最终依赖于一个返回对方法的目的并不重要,并且有很大的改变机会。隐式返回有什么意义?有没有办法让事情变得更简单?总是有返回以防止隐含返回被认为是好的做法吗?我是不是太担心这个了?附言当人们想要从方法中返回特定的东西时,他们是否经常使用隐式返回,这不是让你组中的其他人更容易破坏彼此的代码吗?当然,记录一切并给出

  8. ruby - 怎么来的(a_method || :other) returns :other only when assigning to a var called a_method? - 2

    给定以下方法:defsome_method:valueend以下语句按我的预期工作:some_method||:other#=>:valuex=some_method||:other#=>:value但是下面语句的行为让我感到困惑:some_method=some_method||:other#=>:other它按预期创建了一个名为some_method的局部变量,随后对some_method的调用返回该局部变量的值。但为什么它分配:other而不是:value呢?我知道这可能不是一件明智的事情,并且可以看出它可能有多么模棱两可,但我认为应该在考虑作业之前评估作业的右侧...我已经在R

  9. ruby-on-rails - 使用回形针的嵌套形式 - 2

    我有一个名为posts的模型,它有很多附件。附件模型使用回形针。我制作了一个用于创建附件的独立模型,效果很好,这是此处说明的View(https://github.com/thoughtbot/paperclip):@attachment,:html=>{:multipart=>true}do|form|%>posts中的嵌套表单如下所示:prohibitedthispostfrombeingsaved:@attachment,:html=>{:multipart=>true}do|at_form|%>附件记录已创建,但它是空的。文件未上传。同时,帖子已成功创建...有什么想法吗?

  10. ruby-on-rails - 我该怎么办 :remote location validation with CarrierWave? - 2

    我在我的Rails3示例应用程序上使用CarrierWave。我想验证远程位置上传,因此当用户提交无效URL(空白或非图像)时,我不会收到标准错误异常:CarrierWave::DownloadErrorinImageController#createtryingtodownloadafilewhichisnotservedoverHTTP这是我的模型:classPaintingtrue,:length=>{:minimum=>5,:maximum=>100}validates:image,:presence=>trueend这是我的Controller:classPaintingsC

随机推荐