业务系统的数据,一般最后都会落入到数据库中,例如 MySQL
、Oracle
等主流数据库,不可避免的,在数据更新时,有可能会遇到错误,这时需要将之前的数据更新操作撤回,避免错误数据。
Spring
的声明式事务能帮我们处理回滚操作,让我们不需要去关注数据库底层的事务操作,可以不用在出现异常情况下,在 try / catch / finaly 中手写回滚操作。
Spring
的事务保证程度比行业中其它技术(例如 TCC
/ 2PC
/ 3PC
等)稍弱一些,但使用 Spring
事务已经满足大部分场景,所以它的使用和配置规则也是值得学习的。
接下来一起学习 Spring
事务是如何使用以及实现原理吧。
Table of Contents generated with DocToc
使用例子
1.创建数据库表
1 | create table test.user( |
2.创建对应数据库表的 PO
1 | public class JdbcUser { |
3.创建表与实体间的映射
在使用 JdbcTemplate
时很纠结,在 Java
类中写了很多硬编码 SQL
,与 MyBatis
使用方法不一样,为了示例简单,使用了 JdbcTemplate
,不过还是建议朋友们用 MyBatis
,让代码风格整洁。
1 | public class UserRowMapper implements RowMapper { |
4.创建数据操作接口
1 | public interface UserDao { |
5.创建数据操作接口实现类
跟书中例子不一样,没有在接口上加入事务注解,而是在公共方法上进行添加,可以在每个方法上自定义传播事件、隔离级别。
1 | public class UserJdbcTemplate implements UserDao { |
6.创建配置文件
1 |
|
7.添加依赖
记得添加数据库连接和 jdbc
、tx
这两个 spring
模块的依赖
1 | optional(project(":spring-jdbc")) // for Quartz support |
8.启动代码
1 | public class TransactionBootstrap { |
通过上面的代码,我做了两个测试:
- 配置文件中,没开启事务。 也就是
<tx:annotation-driven/>
这一行被注释了,虽然我们执行的方法中抛出了RuntimeExcepton
,但是数据库中依旧被插入了数据。 - 配置文件中,开启事务。 将上面的注释去掉,删掉数据库中的记录,重新执行启动代码,发现数据没有被插入, 在程序抛出异常情况下,
Spring
成功执行了事务,回滚了插入操作。
注解属性 @Transactional
具体位置在:org.springframework.transaction.annotation.Transactional
属性 | 类型 | 作用 | |
---|---|---|---|
value | String | 可选的限定描述符,指定使用的事务管理器 | |
propagation | 枚举:Propagation | 可选的事务传播行为 | |
isolation | 枚举:Isolation | 可选的事务隔离级别设置 | |
readOnly | boolean | 设置读写或只读事务,默认是只读 | |
rollbackFor | Class 数组,必须继承自 Throwable | 导致事务回滚的异常类数组 | |
rollbackForClassName | 类名称数组,必须继承自 Throwable | 导致事务回滚的异常类名字数组 | |
noRollbackFor | Class 数组,必须继承自 Throwable | 不会导致事务回滚的异常类数组 | |
noRollbackForClassName | 类名称数组,必须继承自 Throwable | 不会导致事务回滚的异常类名字数组 |
事务的传播性 Propagation
- REQUIRED
这是默认的传播属性,如果外部调用方有事务,将会加入到事务,没有的话新建一个。
- PROPAGATION_SUPPORTS
如果当前存在事务,则加入到该事务;如果当前没有事务,则以非事务的方式继续运行。
- PROPAGATION_NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- PROPAGATION_NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。
事务的隔离性 Isolation
- READ_UNCOMMITTED
最低级别,只能保证不读取
物理上损害的数据,允许脏读
- READ_COMMITTED
只能读到已经提交的数据
- REPEATABLE_READ
可重复读
- SERIALIZABLE
串行化读,读写相互阻塞
这里只是简单描述了一下这两个主要属性,因为底层与数据库相关,可以看下我之前整理过的 MySQL锁机制
Spring 中实现逻辑
介绍完如何使用还有关键属性设定,本着知其然,知其所以然的学习精神,来了解代码是如何实现的吧。
解析
之前在解析自定义标签时提到,AOP
和 TX
都使用了自定义标签,按照我们上一篇 AOP
的学习,再来一遍解析自定义标签的套路:事务自定义标签。
定位到 TxNamespaceHandler
类的初始化方法:
1 |
|
根据上面的方法,Spring
在初始化时候,如果遇到诸如 <tx:annotation-driven>
开头的配置后,将会使用 AnnotationDrivenBeanDefinitionParser
解析器的 parse
方法进行解析。
1 | public BeanDefinition parse(Element element, ParserContext parserContext) { |
Spring
中的事务默认是以 AOP
为基础,如果需要使用 AspectJ
的方式进行事务切入,需要在 mode
属性中配置:
1 | <tx:annotation-driven mode="aspectj"/> |
本篇笔记主要围绕着默认实现方式,动态 AOP
来学习,如果对于 AspectJ
实现感兴趣请查阅更多资料~
注册 InfrastructureAdvisorAutoProxyCreator
与 AOP
一样,在解析时,会创建一个自动创建代理器,在事务 TX
模块中,使用的是 InfrastructureAdvisorAutoProxyCreator
。
首先来看,在默认配置情况下,AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext)
做了什么操作:
1 | private static class AopAutoProxyConfigurer { |
在这里注册了代理类和三个 bean
,这三个关键 bean
支撑了整个事务功能,为了待会更好的理解这三者的关联关系,我们先来回顾下 AOP
的核心概念:
- Pointcut
定义一个切点,可以在这个被拦截的方法前后进行切面逻辑。 - Advice
用来定义拦截行为,在这里实现增强的逻辑,它是一个祖先接口org.aopalliance.aop.Advice
。还有其它继承接口,例如MethodBeforeAdvice
,特定指方法执行前的增强。 - Advisor
用来封装切面的所有信息,主要是上面两个,它用来充当Advice
和Pointcut
的适配器。
回顾完 AOP
的概念后,继续来看下这三个关键 bean
:
- TransactionInterceptor: 实现了
Advice
接口,在这里定义了拦截行为。 - AnnotationTransactionAttributeSource:封装了目标方法是否被拦截的逻辑,虽然没有实现
Pointcut
接口,但是在后面目标方法判断的时候,实际上还是委托给了AnnotationTransactionAttributeSource.getTransactionAttributeSource
,通过适配器模式,返回了Pointcut
切点信息。 - TransactionAttributeSourceAdvisor: 实现了
Advisor
接口,包装了上面两个信息。
这三个 bean
组成的结构与 AOP
切面环绕实现的结构一致,所以先学习 AOP
的实现,对事务的了解会有所帮助
接着看我们的自动创建代理器是如何创建的:
AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element)
1 | public static void registerAutoProxyCreatorIfNecessary( |
在这一步中,注册了一个 beanName
是 org.springframework.aop.config.internalAutoProxyCreator
的 bean
:InfrastructureAdsivorAutoProxyCreator
,下图是它的继承体系图:
可以看到,它实现了 InstantiationAwareBeanPostProcessor
这个接口,也就是说在 Spring
容器中,所有 bean
实例化时,Spring
都会保证调用其 postProcessAfterInitialization
方法。
与上一篇介绍的 AOP
代理器一样,在实例化 bean
的时候,调用了代理器父类 AbstractAutoProxyCreator
的 postProcessAfterInitialization
方法:
1 | public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { |
其中关于 wrapIfNecessory
方法,在上一篇 AOP
中已经详细讲过,这里讲下这个方法做了什么工作:
- 找出指定
bean
对应的增强器 - 根据找出的增强器创建代理
与创建 AOP
代理相似的过程就不再重复说,讲下它们的不同点:
判断目标方法是否适合 canApply
AopUtils#canApply(Advisor, Class<?>, boolean)
1 | public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) { |
我们在前面看到,TransactionAttributeSourceAdvisor
的父类是 PointcutAdvisor
,所以在目标方法判断的时候,会取出切点信息 pca.getPointcut()
。
我们之前注入的切面类型 bean
是 AnnotationTransactionAttributeSource
,通过下面的方法包装,最后返回对象类型是 TransactionAttributeSourcePointcut
的切点信息
1 | private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { |
匹配标签 match
在匹配 match
操作中,区别的是 AOP
识别的是 @Before
、@After
,而我们的事务 TX
识别的是 @Transactional
标签。
判断是否是事务方法的入口方法在这:
org.springframework.transaction.interceptor.TransactionAttributeSourcePointcut#matches
1 |
|
那它到底到哪一步解析事务注解的呢,继续跟踪代码,答案是:
AnnotationTransactionAttributeSource#determineTransactionAttribute
1 | protected TransactionAttribute determineTransactionAttribute(AnnotatedElement element) { |
在这一步中,遍历注册的注解解析器进行解析,由于我们关注的是事务解析,所以直接定位到事务注解的解析器:
SpringTransactionAnnotationParser#parseTransactionAnnotation(AnnotatedElement)
1 | public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) { |
首先判断是否含有 @Transactional
注解,如果有的话,才继续调用 parse
解析方法:
1 | protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) { |
小结
通过上面的步骤,完成了对应类或者方法的事务属性解析。
主要步骤在于寻找增强器,以及判断这些增强器是否与方法或者类匹配。
如果某个 bean
属于可被事务增强时,也就是适用于增强器 BeanFactoryTransactionAttributeSourceAdvisor
进行增强。
之前我们注入了 TransactionInterceptor
到 BeanFactoryTransactionAttributeSourceAdvisor
中,所以在调用事务增强器增强的代理类时,会执行 TransactionInterceptor
进行增强。同时,也就是在 TransactionInterceptor
类中的 invoke
方法中完成整个事务的逻辑。
运行
事务增强器 TransactionInterceptor
TransactionInterceptor
支撑着整个事务功能的架构。跟之前 AOP
的 JDK
动态代理 分析的一样,TransactionInterceptor
拦截器继承于 MethodInterceptor
,所以我们要从它的关键方法 invoke()
看起:
1 | public Object invoke(MethodInvocation invocation) throws Throwable { |
实际调用了父类的方法:TransactionAspectSupport#invokeWithinTransaction
1 | protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, |
贴出的代码有删减,简略了错误异常的 try / catch
和编程式事务处理的逻辑。因为我们更多使用到的是声明式事务处理,就是在 XML
文件配置或者 @Transactional
注解编码,实际通过 AOP
实现,而编程式事务处理是通过 Transaction Template
实现,比较少使用到,所以省略了这部分处理代码。
事务管理器
通过该方法,确定要用于给定事务的特定事务管理器
TransactionAspectSupport#determineTransactionManager
1 | protected PlatformTransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) { |
由于最开始我们在 XML
文件中配置过 transactionManager
属性,所以该方法在我们例子中将会返回类型是 DataSourceTransactionManager
的事务管理器,下面是 DataSourceTransactionManager
的继承体系:
它实现了 InitializingBean
接口,不过只是在 afterPropertiesSet()
方法中,简单校验 dataSource
是否为空,不细说这个类。
事务开启
TransactionAspectSupport#createTransactionIfNecessary
1 | protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) { |
在创建事务方法中,主要完成以下三件事:
- 使用
DelegatingTransactionAttribute
包装txAttr
实例 - 获取事务:
tm.getTransaction(txAttr)
- 构建事务信息:
prepareTransactionInfo(tm, txAttr, joinpointIdentification, status)
核心方法在第二点和第三点,分别摘取核心进行熟悉。
获取 TransactionStatus
status = tm.getTransaction(txAttr);
由于代码较长,直接来总结其中几个关键点
获取事务
创建对应的事务实例,我们使用的是 DataSourceTransactionManager
中的 doGetTransaction
方法,创建基于 JDBC
的事务实例。
1 | protected Object doGetTransaction() { |
其中在同一个线程中,判断是否有重复的事务,是在 TransactionSynchronizationManager.getResource(obtainDataSource())
中完成的,关键判断逻辑是下面这个:
1 | private static final ThreadLocal<Map<Object, Object>> resources = |
结论:resources
是一个 ThreadLocal
线程私有对象,每个线程独立存储,所以判断是否存在事务,判断的依据是当前线程、当前数据源(DataSource)中是否存在活跃的事务 - map.get(actualKey)
。
处理已经存在的事务
根据前面说的,判断当前线程是否存在事务,判断依据为当前线程记录的连接不为空且连接中(connectionHolder)中的 transactionActive
属性不为空,如果当前线程存在事务,将根据不同的事务传播特性进行处理。具体代码逻辑如下:
1 | if (isExistingTransaction(transaction)) { |
PROPAGATION_NEVER
在配置中配置设定为 PROPAGATION_NEVER
,表示该方法需要在非事务的环境下运行,但处于事务处理的状态(可能是外部带事务的方法调用了非事务的方法),将会抛出异常:1
2
3
4if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
throw new IllegalTransactionStateException(
"Existing transaction found for transaction marked with propagation 'never'");
}
PROPAGATION_NOT_SUPPORTED
如果有事务存在,将事务挂起,而不是抛出异常:
1 | if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { |
事务挂起
对于挂起操作,主要目的是记录原有事务的状态,以便于后续操作对事务的恢复:
实际上,suspend()
方法调用的是事务管理器 DataSourceTransactionManager
中的 doSuspend()
方法
1 | protected Object doSuspend(Object transaction) { |
最后调用的关键方法是 TransactionSynchronizationManager#doUnbindResource
1 | private static Object doUnbindResource(Object actualKey) { |
看了第七条参考资料中的文章,结合代码理解了事务挂起的操作:移除当前线程、数据源活动事务对象的一个过程
那它是如何实现事务挂起的呢,答案是在 doSuspend()
方法中的 txObject.setConnectionHolder(null)
,将 connectionHolder
设置为 null
。
一个 connectionHolder
表示一个数据库连接对象,如果它为 null
,表示在下次需要使用时,得从缓存池中获取一个连接,新连接的自动提交是 true
。
PROPAGATION_REQUIRES_NEW
表示当前方法必须在它自己的事务里运行,一个新的事务将被启动,而如果有一个事务正在运行的话,则这个方法运行期间被挂起。
1 | SuspendedResourcesHolder suspendedResources = suspend(transaction); |
与前一个方法相同的是,在 PROPAGATION_REQUIRES_NEW
广播特性下,也会使用 suspend
方法将原事务挂起。方法 doBegin()
,是事务开启的核心。
PROPAGATION_NESTED
表示如果当前正有一个事务在运行中,则该方法应该运行在一个嵌套的事务中,被嵌套的事务可以独立于封装事务进行提交或者回滚,如果封装事务不存在,行为就像 PROPAGATION_REQUIRES_NEW
。
在代理处理上,有两个分支,与 PROPAGATION_REQUIRES_NEW
相似的不贴出来,讲下使用 savepoint
保存点的方式事务处理:
1 | if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { |
学习过数据库的朋友应该清楚 savepoint
,可以利用保存点回滚部分事务,从而使事务处理更加灵活和精细。跟踪代码,发现创建保存点调用的方法是 org.hsqldb.jdbc.JDBCConnection#setSavepoint(java.lang.String)
,感兴趣的可以往下继续深入学习~
事务创建
其实在前面方法中,都出现过这个方法 doBegin()
,在这个方法中创建事务,顺便设置数据库的隔离级别、timeout
属性和设置 connectionHolder
:
DataSourceTransactionManager#doBegin
1 | protected void doBegin(Object transaction, TransactionDefinition definition) { |
结论:Spring 事务的开启,就是将数据库自动提交属性设置为 false
小结
在声明式的事务处理中,主要有以下几个处理步骤:
- 获取事务的属性:
tas.getTransactionAttribute(method, targetClass)
- 加载配置中配置的
TransactionManager
:determineTransactionManager(txAttr);
- 不同的事务处理方式使用不同的逻辑:关于声明式事务和编程式事务,可以查看这篇文章-Spring编程式和声明式事务实例讲解
- 在目标方法执行前获取事务并收集事务信息:
createTransactionIfNecessary(tm, txAttr, joinpointIdentification)
- 执行目标方法:
invocation.proceed()
- 出现异常,尝试异常处理:
completeTransactionAfterThrowing(txInfo, ex);
- 提交事务前的事务信息消除:
cleanupTransactionInfo(txInfo)
- 提交事务:
commitTransactionAfterReturning(txInfo)
事务回滚 & 提交
这两步操作,主要调用了底层数据库连接的 API
,所以没有细说。
总结
本篇文章简单记录了如何使用 Spring
的事务,以及在代码中如何实现。
在之前的使用场景中,只用到了默认配置的声明式事务 @Transactional
,不了解其它属性设置的含义,也不知道在默认配置下,如果是同一个类中的方法自调用是不支持事务。
所以,经过这一次学习和总结,在下一次使用时,就能够知道不同属性设置能解决什么问题,例如修改广播特性 PROPAGATION
,让事务支持方法自调用,还有设置事务超时时间 timeout
、隔离级别等属性。
由于个人技术有限,如果有理解不到位或者错误的地方,请留下评论,我会根据朋友们的建议进行修正
Gitee 地址 https://gitee.com/vip-augus/spring-analysis-note.git
Github 地址 https://github.com/Vip-Augus/spring-analysis-note