SpringBoot(Spring) 事务
2021-12-22 22:05:50 27 举报
AI智能生成
spring事务
作者其他创作
大纲/内容
1.案例演示
用maven构建的springboot项目
github
https://github.com/HackerFight/tx-demo.git
gitee
https://gitee.com/hacker9/tx-demo.git
重点关注 UserService
2.原理探究
1.使用事务的前提条件
1.在配置类上添加 @EnableTransactionManagement 注解
做了什么?
1.给容器导入了一个配置类 ProxyTransactionManagementConfiguration
1.导入了 TransactionAttributeSource
AnnotationTransactionAttributeSource
创建这个对象时,会在其内部创建一个 SpringTransactionAnnotationParser 对象,将会去解析 @Transactional 注解
2.导入了 TransactionInterceptor
implements MethodInterceptor,Advice
这个拦截器非常重要
3.导入了 BeanFactoryTransactionAttributeSourceAdvisor
beanName: org.springframework.transaction.config.internalTransactionAdvisor
implements PointcutAdvisor
设置 TransactionAttributeSource 属性
就是1中导入的对象
设置 Advice 属性
就是 2中导入的对象
2.给容器导入一个 处理器 InfrastructureAdvisorAutoProxyCreator
1.直译过来就是 基础增强自动代理创造器
2.继承关系
AbstractAdvisorAutoProxyCreator
AbstractAutoProxyCreator
BeanPostProcessor
3.不难发现,它确实是一个后置处理器,其中 AbstractAutoProxyCreator 非常的重要!!!
它将后置方法是我们分析的入口和重点
2.在业务类上或者业务类的方法中添加 @Transactional 注解
2.原理探究
1.spring的后置处理器在bean初始化后将会进行处理器方法的调用
BeanPostProcessor#postProcessAfterInitialization
我们要关注的就是:AbstractAutoProxyCreator
1.这个处理器也是 SmartInstantiationAwareBeanPostProcessor 类型,将在实例化前进行一次调用,有机会返回一个对象,一旦有对象返回,那么将不会在经过spring对象创建的三大步骤:实例-属性赋值-初始化 过程,所以这个方法可以忽略
2.三个Map属性分析
earlyProxyReferences
这个是循环引用会用到的,可以忽略
advisedBeans
这个很重要,我们知道所有的处理器是针对所有的bean的,所以说这个属性其实会存储所有的bean(不包括上面循环引用的),如果value=true,则就是我们需要增强的bean
proxyTypes
这个属性也可以忽略,主要是获取类型的
3.一个Set属性
targetSourcedBeans
参考步骤1,如果创建了对象,则将会保存到这里,这个也可以忽略
2.AbstractAutoProxyCreator#postProcessAfterInitialization() 方法分析
1.处理器是针对所有的bean的,所以可以打一个条件断点,当userService初始化后来到这里
2.发现符合条件的增强器(findEligibleAdvisors(beanClass, beanName))
1.什么是增强器?(Advisor)
这个接口的翻译:保存AOP通知(在连接点上采取的操作)的基本接口和确定通知适用性的过滤器(如切入点)。其中通知就是我们的方法,比如,@Transaction 注解标注的方法就可以理解为一个通知,或者AspectJ 中的前置通知,后置通知等等,这些都是方法,只不过是有一点特殊的方法
2.发现候选的增强器(findCandidateAdvisors())
返回的是一个集合,但实际上就一个,就是前面 @EnableTransactionManagement 注解导入的 BeanFactoryTransactionAttributeSourceAdvisor
3.发现可以用的增强器(findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName))
1.第一个参数就是前面发现的候选增强器,第二个参数就是 com.qiuguan.tx.UserService, 第三个参数就是 userService
2.通过遍历所有的候选增强器(这里实际上就一个)和我们的当前业务类UserService 去找可用的增强器(canApply(candidate, clazz, hasIntroductions))
1.因为候选增强器 BeanFactoryTransactionAttributeSourceAdvisor 是一个PointcutAdvisor 类型,所以进行强转
2.强转后获取切点
1.用来标识对什么方法进入代理
2.是 TransactionAttributeSourcePointcut 类,直接new的
3.遍历当前业务类的所有方法,一旦发现有一个方法标注了 @Transactional 注解,则表示发现了可用的增强器
3.一旦发现可用,这个理解起来就是:通过当前候选增强器去解析当前业务bean,在当前业务bean中找到了 @Transactional 注解标注的方法,就表示找到了),则将当前候选增强器放到可用增强器集合中。就是 BeanFactoryTransactionAttributeSourceAdvisor
3.一旦有符合条件的增强器
1.将当前业务bean 放到 advisedBeans 属性中,value=TRUE, 标志这个bean 是一个要增强的bean
2.创建代理对象
1.创建 ProxyFactory 对象
2.获取代理对象
1.通过 AopProxyFactory 去获取
DefaultAopProxyFactory
2.根据 DefaultAopProxyFactory 一个具体的代理对象,这里用到了简单工厂模式
JdkDynamicAopProxy
基于JDK的动态代理,需要实现接口
ObjenesisCglibAopProxy
基于Cglib的,基于修改字节码文件,派生子类
我这里没有实现接口,所以使用的是Cglib的动态代理
用到的拦截器是:DynamicAdvisedInterceptor
当调用业务类UserService 中的方法时,将会被它拦截,来到 intercept 方法中
3.将代理类放入 proxyTypes 属性中
4.返回步骤2中的代理对象
4.如果没有符合条件的增强器,则说明当前业务bean不需要包装,直接返回bean即可
3.上面步骤执行完了,我觉得主要做了一件事
当前业务bean需要增强,返回一个代理对象
当调用业务方法,来到代理对象的拦截方法
4.当调用业务方法
1.获取拦截器调用链(集合)
1.为什么是链(集合)?
因为AspectJ 也是会来到这里,它可以有前置,后置,返回等等,所以有链;而我这里就是一个普通的事务,所以它返回的就是一个
2.遍历所有的增强器,实际上这里就一个 BeanFactoryTransactionAttributeSourceAdvisor
3.如果发现了 @Transactional 注解的方法,则将当前增强器转换为 MethodInterceptor 拦截器
1.获取当前增强器中的 Advice, 将其强转为 MethodInterceptor
这个Advice 就是前面配置类导入的 TransactionInterceptor
2.如果是AspectJ, 则也将前置,后置等适配为 MethodInterceptor
2.如果拦截器链是空的并且方法时public的,那么说明调用的是cglib代理类中一个普通方法
就是调用了UserService 中没有标注 @Transactional 注解的方法,直接调用即可
3.如果拦截器链不为空,则创建一个 CglibMethodInvocation 对象,并调用 proceed() 方法
1.获取事务管理器 PlatformTransactionManager
这个类是对事务操作一个非常重要的类
2.设置自动提交为 false
在 createTransactionIfNecessary() 方法中,可以看他上面的注释
继续调用 tm.getTransaction(txAttr)
这里将会把注解的全部属性映射到 TransactionManager 中
最终是映射到 java.sql.Connection 中
3.开始调目标用方法
这里请看注释:这是一个around建议:调用链中的下一个拦截器。这通常会导致调用目标对象。
事务和AspectJ 本质上一样的,只不过事务显得普通一点而已
4.如果成功,则commit
5.如果失败,则rollback
3.动态代理演示
1.Cglib & JDK
2.github
https://github.com/HackerFight/proxy.git
3.gitee
https://gitee.com/hacker9/proxy.git
4.使用声明式事务与编程式事务
1.声明式事务
这个是最简单的,也是最常用,只需要使用 @Transaciton 和 @EnableTransactionManagement 注解即可
2.编程式事务
可以通过事务模板
TransactionTemplate
可以通过事务管理器
TransactionManager
5.事务属性探究
1.Isolation 指定事务的隔离级别
1.@Transactional(isolation = Isolation.DEFAULT)
采用数据库的隔离级别:(MySQL: 可重复读、Oracle: 读已提交)
最好就使用这个参数,默认也是这个参数
2.@Transactional(isolation = Isolation.READ_UNCOMMITTED)
3.@Transactional(isolation = Isolation.READ_COMMITTED)
4.@Transactional(isolation = Isolation.REPEATABLE_READ)
5.@Transactional(isolation = Isolation.SERIALIZABLE)
2.readOnly 设置是否是只读事务(常用,用来标识是否是只读操作)
当你能确保整个事务过程中只对数据库执行Select操作的时候,如果将此属性设置为true,则会自动进行优化,提高性能。
如果这只了true, 但是业务方法中有 增删改操作,则会报错
3.timeout 设置事务的超时时间(单位:秒)
0.开启事务后,会通过 TransactionAspectSupport#invokeWithinTransaction#createTransactionIfNecessary#getTransaction
AbstractPlatformTransactionManager#getTransaction#startTransaction#doBegin
在这里会将超时时间转换成毫秒,然后加上当前时间,转换为Date类型,赋给截止时间
this.deadline = new Date(System.currentTimeMillis() + millis)
1.当使用 JdbcTemplate 执行sql语句时,会将超时时间设置到 PreparedStatement 中的 queryTimeout
2.我把超时时间设置的很小,比如2秒,然后我去debug, 发现当设置超时时间的时候,就直接在finally 块中关闭了链接,没有去执行sql语句
这就说明,在执行sql之前,以及sql执行中,这两部分花费的时间都是计算到 timeout 中
3.超时生效与不生效的具体演示
4.默认值是-1,取数据库的事务超时时间
4.rollbackFor 异常回滚
1.导致事务回滚的异常类数组
2.默认是empty的,此时只有抛出 RuntimeException 或者 Error 类型的异常才会回滚
DefaultTransactionAttribute#rollbackOn
(ex instanceof RuntimeException || ex instanceof Error)
3.如果我rollbackFor=NullPointerException.class, 但实际上抛出的是数学异常 ArithmeticException,此时会怎么办?
他会根据实际发生的异常去向上匹配
1.如果rollbackFor 异常和实际发生的异常一样,则肯定回滚
2.如果不一样,则递归获取实际发生的异常的父异常和rollbackFor异常匹配,如果匹配了,则肯定回滚,如果递归到了Throwable, 则确实没有匹配,此时就调用父类的rollbackOn 方法,就是前面说的 (ex instanceof RuntimeException || ex instanceof Error)
3.实际上这里的 NullPointerException 异常是没用的,最终还是因为实际发生的异常是RuntimeException 而进行回滚
4.阿里巴巴开发规约中,建议我们将 rollbackFor=Exception.class, 为什么呢?
异常可以分为受检异常和非受检异常,对于非受检异常,比如 IOException, FileNotFundException 等等,这些异常就算抛出来了,也不会回滚的,此时如果 rollbackFor=Exception.class 则可以回滚, 指定这个的目的就是让所有异常都进行回滚
5.norollbackFor 异常不回滚
不会导致事务回滚的异常类数组
和上面相反,这块的关于回滚和不回滚的设计可以学习一下
RuleBasedTransactionAttribute#rollbackOn
6.propagation 事务的传播行为
1.事务传播行为是Spring框架提供的一种事务管理方式,它不是数据库提供的
2.事务的传播行为主要是应对 事务方法嵌套事务方法
3.事务传播行为的类型
PROPAGATION_REQUIRED
这个是默认选项
如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中(共用了同一个事务)。这是最常见的选择。
PROPAGATION_REQUIRES_NEW
新建事务,如果当前存在事务,把当前事务挂起
PROPAGATION_NESTED
如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
PROPAGATION_SUPPORTS
假设当前在事务中。即以事务的形式执行。假设当前不在一个事务中,那么就以非事务的形式执行
PROPAGATION_MANDATORY
必须在一个事务中执行。也就是说,他仅仅能被一个父事务调用。否则,他就要抛出异常
PROPAGATION_NOT_SUPPORTED
当前不支持事务。比方ServiceA.methodA的事务级别是PROPAGATION_REQUIRED 。而ServiceB.methodB的事务级别是PROPAGATION_NOT_SUPPORTED ,那么当执行到methodA 的ServiceB.methodB时。ServiceA.methodA的事务挂起。而他以非事务的状态执行完,再继续ServiceA.methodA的事务
PROPAGATION_NEVER
不能在事务中执行。就是不能被事务方法调用,否则抛出异常
4.小总结
1.在不存在事务的情况下:PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED 都是新开启一个事务
开启新的事务意味着开启了新的连接
2.在已经存在事务的情况下,只有 PROPAGATION_REQUIRES_NEW 会开启新的事务
3.在外围方法开启事务的情况下,Propagation.NESTED修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务
4.在外围方法未开启事务的情况下Propagation.NESTED和Propagation.REQUIRES_NEW 作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
5.在外围方法开启事务的情况下Propagation.REQUIRES_NEW修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。
6.在外围方法开启事务的情况下,Propagation.REQUIRED修饰的内部方法会加入到外围方法的事务中,所有Propagation.REQUIRED修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚。
5.参考测试用例
子主题
6.参考文档
6.事务失效的场景
1.方法不是public
2.方法发生了自身调用(异步也是同样的道理)
事务的本质是代理对象去工作,如果一个@Transacitonal 注解标注的方法a,内部调用了@Transactional 注解标注的方法b,那么此时方法b就是一个普通的方法(本质上就是没有经过代理对象去调用),方法a没有标注@Transactional 注解也一样
3.数据库引擎不支持事务-MyISAM
4.异常进行了catch, spring 是通过抛出异常来进行回滚和提交的
7.面试题整理
0 条评论
下一页