盛年不重来,一日难再晨,及时当勉励,岁月不待人。这篇文章主要讲述#yyds干货盘点# Spring嵌套事务是怎么回滚的?相关的知识,希望能为你提供帮助。
用户注册完成后,需要给该用户登记一门PUA必修课,并更新该门课的登记用户数。
为此,我添加了两个表。
课程表 course,记录课程名称和注册的用户数。
用户选课表 user_course,记录用户表 user 和课程表 course 之间的多对多关联。
同时为课程表初始化了一条课程信息
接下来我们完成用户的相关操作,主要包括两部分:
新增业务类 CourseService实现相关业务逻辑,分别调用了上述方法保存用户与课程的关联关系,并给课程注册人数+1
为避免注册课程的业务异常导致用户信息无法保存,这里 catch 注册课程方法中抛出的异常。希望当注册课程发生错误时,只回滚注册课程部分,保证用户信息依然正常。
为验证异常是否符合预期,在 regCourse() 里抛一个注册失败异常:
最后用户和选课的信息都被回滚了,显然这不符预期。
期待结果是即便内部事务regCourse()发生异常,外部事务saveStudent()俘获该异常后,内部事务应自行回滚,不影响外部事务。
这是什么原因造成的呢?
源码解析伪代码梳理整个事务的结构:
整个业务包含2层事务:
Spring声明式事务中的propagation属性,表示对这些方法使用怎样的事务,即:
一个带事务的方法调用了另一个带事务的方法,被调用的方法它怎么处理自己事务和调用方法事务之间的关系。
propagation 有7种配置:
因为:
默认值,如果本来有事务,则加入该事务,如果没有事务,则创建新的事务。
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():
条件得到满足,执行
默认 false,当前场景为 false
所以,就只由该方法来确定了,默认值为 true, 即是否回滚交由外层事务统一决定
DataSourceTransactionManager#doSetRollbackOnly
最终调用
DataSourceTransactionObject#setRollbackOnly()
内层事务操作执行完毕。
外层事务外层事务中,业务代码就捕获了内层所抛异常,所以该异常不会继续往上抛,最后的事务会在 ??TransactionAspectSupport.invokeWithinTransaction()?
? 中的
TransactionAspectSupport#commitTransactionAfterReturning()
该方法里执行了commit 操作:
AbstractPlatformTransactionManager#commit
当满足 ??!shouldCommitOnGlobalRollbackOnly() &
&
defStatus.isGlobalRollbackOnly()?
?,就会回滚,否则继续提交事务:
DataSourceTransactionObject#isRollbackOnly()
若发现事务被标记了全局回滚,且在发生全局回滚时,判断是否应该提交事务,这个方法的默认返回 false,这里无需关注
该方法最终进入
之前内部事务处理最终调用到DataSourceTransactionObject#setRollbackOnly()
public void setRollbackOnly() {
getConnectionHolder().setRollbackOnly();
}
两个方法本质都是对??ConnectionHolder.rollbackOnly?
?属性标志位的存取
但ConnectionHolder则存在于DefaultTransactionStatus#transaction属性。
综上:外层事务是否回滚的关键,最终取决于DataSourceTransactionObject#isRollbackOnly(),该方法返回值正是在内层异常时设置的。
所以最终外层事务也被回滚,从而在控制台中打印上述日志。
这就明白了,Spring默认事务传播属性为REQUIRED:若已有事务,则加入该事务,若无事务,则创建新事务,因而内外两层事务都处于同一事务。
在 regCourse()中抛异常,并触发回滚操作时,这个回滚会继续传播,从而把 saveUser() 也回滚,最终整个事务都被回滚!
修正Spring事务默认传播属性 REQUIRED,在整个事务的调用链上,任一环节抛异常都会导致全局回滚。
所以只需将传播属性改成 REQUIRES_NEW :
运行:
异常正常抛出,注册课程部分的数据没有保存,但用户还是正常注册成功。这意味着此时Spring 只对注册课程这部分的数据进行了回滚,并没有传播到外层:
【#yyds干货盘点# Spring嵌套事务是怎么回滚的()】?TransactionAspectSupport.invokeWithinTransaction()?
?? 中调用 ??createTransactionIfNecessary()?
? 就会创建一个新的事务,独立于外层事务
推荐阅读
- #yyds干货盘点#Spring专题「实战系列」Spring Security原理以及实战认证分析开发指南
- #yyds干货盘点#?并发技术系列「Web请求读取系列」如何构建一个可重复读取的Request的流机制
- LINUX:FTP服务
- #yyds干货盘点#Java ASM系列((097)生成Control Flow Graph)
- Linux的进程和计划任务管理
- #yyds干货盘点#Redis之Stream
- LINUX文件系统及日志分析
- 删除目录下三天以前的所有的目录下的文件
- #yyds干货盘点#?Java深层系列「技术盲区」让我们一起完全吃透针对于时间和日期相关的API指南