Spring嵌套事务是怎样回滚的?
- 事务的传播机制
- 多数据源的切换问题
更深入了解 Spring 事务。
客户注册完成后,需要给该客户登记一门PUA必修课,并升级该门课的登记客户数。
为此,我增加了两个表。
课程表 course,记录课程名称和注册的客户数。
image
客户选课表 user_course,记录客户表 user 和课程表 course 之间的多对多关联。
image
同时为课程表初始化了一条课程信息image
接下来我们完成客户的相关操作,主要包括两部分:
新添加客户选课记录
image
课程登记学生数 + 1
image
新添加业务类 CourseService实现相关业务逻辑,分别调用了上述方法保存客户与课程的关联关系,并给课程注册人数+1
image
为避免注册课程的业务异常导致客户信息无法保存,这里 catch 注册课程方法中抛出的异常。希望当注册课程发生错误时,只回滚注册课程部分,保证客户信息仍然正常。
image
为验证异常能否符合预期,在 regCourse() 里抛一个注册失败异常:image
执行代码:
image注册失败部分的异常符合预期,但是后面又多了一个这样的错误提醒:Transaction rolled back because it has been marked as rollback-only
image
最后客户和选课的信息都被回滚了,显然这不符预期。
期待结果是即使内部事务regCourse()发生异常,外部事务saveStudent()俘获该异常后,内部事务应自行回滚,不影响外部事务。
这是什么起因造成的呢?
源码解析
伪代码梳理整个事务的结构:
image
整个业务包含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()
进行异常解决:
image
对异常类型做了少量检查,当符合公告中的定义后,执行具体的 rollback 操作,这个操作是通过如下方法完成:
AbstractPlatformTransactionManager
rollback()
该回滚实现负责解决正参加到已有事务集的事务。委托执行Rollback和doSetRollbackOnly。
image
继续调用
processRollback()
image
该方法里区分了三种场景:
- 能否有保存点
- 能否为一个新的事务
- 能否处于一个更大的事务中
由于默认传播类型REQUIRED,嵌套的事务并未开启一个新事务,所以属于当前事务处于一个更大事务中,所以会走到分支1。
如下的判断条件确定能否设置为仅回滚:
if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure())
满足任一,都会执行 doSetRollbackOnly():
isLocalRollbackOnly
image
默认 false,当前场景为 false
isGlobalRollbackOnParticipationFailure()
image
所以,就只由该方法来确定了,默认值为 true, 即能否回滚交由外层事务统一决定
条件得到满足,执行
DataSourceTransactionManager#doSetRollbackOnly
image
最终调用
DataSourceTransactionObject#setRollbackOnly()
image
内层事务操作执行完毕。
外层事务
外层事务中,业务代码就捕获了内层所抛异常,所以该异常不会继续往上抛,最后的事务会在 TransactionAspectSupport.invokeWithinTransaction()
中的
TransactionAspectSupport#commitTransactionAfterReturning()
image
该方法里执行了commit 操作:
AbstractPlatformTransactionManager#commit
image
当满足 !shouldCommitOnGlobalRollbackOnly() &&defStatus.isGlobalRollbackOnly()
,就会回滚,否则继续提交事务:
- shouldCommitOnGlobalRollbackOnly()
若发现事务被标记了全局回滚,且在发生全局回滚时,判断能否应该提交事务,这个方法的默认返回 false,这里无需关注 isGlobalRollbackOnly()
image
该方法最终进入
DataSourceTransactionObject#isRollbackOnly()
image
之前内部事务解决最终调用到DataSourceTransactionObject#setRollbackOnly()
public void setRollbackOnly() { getConnectionHolder().setRollbackOnly();}
- isRollbackOnly()
- setRollbackOnly()
两个方法本质都是对ConnectionHolder.rollbackOnly
属性标志位的存取
但ConnectionHolder则存在于DefaultTransactionStatus#transaction属性。
综上:外层事务能否回滚的关键,最终取决于DataSourceTransactionObject#isRollbackOnly(),该方法返回值正是在内层异常时设置的。
所以最终外层事务也被回滚,从而在控制台中打印上述日志。
这就明白了,Spring默认事务传播属性为REQUIRED:若已有事务,则加入该事务,若无事务,则创立新事务,因此内外两层事务都处于同一事务。
在 regCourse()中抛异常,并触发回滚操作时,这个回滚会继续传播,从而把 saveUser() 也回滚,最终整个事务都被回滚!
修正
Spring事务默认传播属性 REQUIRED,在整个事务的调用链上,任一环节抛异常都会导致全局回滚。
所以只要将传播属性改成 REQUIRES_NEW :
image
运行:image
异常正常抛出,注册课程部分的数据没有保存,但客户还是正常注册成功。这意味着此时Spring 只对注册课程这部分的数据进行了回滚,并没有传播到外层:
- 当子事务公告为 Propagation.REQUIRES_NEW 时,在
TransactionAspectSupport.invokeWithinTransaction()
中调用createTransactionIfNecessary()
就会创立一个新的事务,独立于外层事务 - 而在 AbstractPlatformTransactionManager.processRollback() 进行 rollback 解决时,由于 status.isNewTransaction() 会由于它处于一个新的事务中而返回 true,所以它走入到了另一个分支,执行了 doRollback() 操作,让这个子事务单独回滚,不会影响到主事务。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Spring嵌套事务是怎样回滚的?