🍃Spring源码阅读-事务

type
status
date
slug
summary
tags
category
icon
password
原文
spring 事务实现涉及动态代理、AOP拦截器、事务管理器(PlatformTransactionManager)以及底层数据库(jdbc)的协同工作。
spring IOC 容器初始化时,会识别是否启用事务,如果启用,容器会在处理容器内部组件加入
事务相关的容器 Bean(BeanFactoryTransactionAttributeSourceAdvisorTransactionInterceptor 等),在 Bean 初始化后置处理中将 Bean以代理对象替换原有对象,创建代理流程大致同 AOP创建代理对象一致,只是带事务的代理Bean所持有的对象有所不同。
这里展示了一个Jdk动态代理的一个 Service,其中拦截链路advisors 中相关的对象是事务相关的Bean。
AOP的处理流程也有 advice(切点和切面)、MethodInterceptor(多种 Before、after 等),在处理事务的代理 Bean 中,这里不过是替换成事务需要的 advice,MethodInterceptor(TransactionInterceptor)。

声明式工作流程简述

1.启用事务

当使用@EnableTransationManagement 注解启动事务时,Spring 通过TransationManagementConfiguraigurationSelector 导入两个核心组件:
  • AutoProxyRegistrar:获取@EnableTransationManagement注解中的配置,注册 InfrastructureAdvisorAutoProxyCreator,负责为 Bean 创建 AOP 代理。
    • 💡
      在Spring IOC容器初始化代理创建器只会存在一个名为 org.springframework.aop.config.internalAutoProxyCreator 的代理创建器,事务、切面等都会触发IOC组件初始化的操作。如果应用同时开启了事务、AOP,Spring IOC 容器会在初始化内部组件时(refresh()#egisterBeanPostProcessors)会有重复注册内部组件的逻辑,如果有已经创建的代理创建器会根据优先级进行替换。
  • ProxyTransactionManagementConfiguration:配置事务拦截器和切面,定义相关Bean(如 TransactionInterceptor 和 BeanFactoryTransactionAttributeSourceAdvisor)
    • ProxyTransactionManagementConfiguration 是一个带有 @Configuration 注解的配置类,其作用是注册启用注解驱动事务管理所需的 Spring 基础设施 Bean。
      声明式事务也是一种切面,ProxyTransactionManagementConfiguration 会封装关于事务切面的几个核心类
    • BeanFactoryTransactionAttributeSourceAdvisor 事务的 Advisor,也就是封装了事务的拦截逻辑,应用到带有 @Transactional 的方法上。和AOP Advisor 功能类似,只不过事务的切面有所不同,事务只有用户只会定义Pointcut,通知(advice)后执行由具体的事务的拦截器TransactionInterceptor 来决定而 AOP 是用户自定义的逻辑
    • TransactionInterceptor 负责在方法调用前后自动开启、提交或回滚事务。

2.注解扫描

AnnotationTransactionAttributeSource扫描哪些类、方法上持有 @Transactional 注解,并将其转换为事务属性TransactionAttribute), TransactionAttribute 是描述目标类和方法上的@Transactional 注解的属性(见TransationalAttribute示例)。
这一步是在Bean 初始化后置处理阶段做这些逻辑处理。
 
TransactionAttribute 示例

注解的优先级

@Transational注解的解析优先级:
  1. 方法上注解:优先使用方法上的@Transational注解配置
  1. 类级别注解:若方法上没有注解,则使用类上的注解
  1. 接口注解:若通过 jdk动态代理,接口上的注解也会被解析(需配置 @EnableTransactionManagement(proxyTargetClass = false)

3.创建动态代理对象

与 Spring 创建动态代理对象流程一致,只不过部分包装代理对象切面、切点、拦截器是事务相关的。这里说的动态代理对象指的是包含@Transactional 的Bean创建代理对象如下定义了一个 Service,容器会创建一个Bean name 为 sysUserServiceImpl 的代理对象。(这里是开启事务的情况下创建代理对象的,即使没有开启 AOP)

事务方法的执行

在代理对象执行相关事务的方法时,会触TransactionInterceptor#invokeWithinTransaction方法,在这个方法中会获取方法上注解上事务属性,然后去创建事务(createTransactionIfNecessary),”事务“相关的行为(事务信息处理、传播行为等)准备好后,去执行目标方法,根据目标方法的执行结果和是否抛出异常然后执行对应的操作(commit 或 rollback)。

1. 事务方法的触发

一个 Service Bean 已经是代理对象,以 Jdk动态代理为例,一个JdkDynamicAopProxy
包装的Service Bean,在执行其方法时触发的是JdkDynamicAopProxy#invoke 方法,通过事务 Bean 的拦截器是 TransactionMethoder,这里会触发TransactionAspectSupport@invokeWithinTransaction 方法的调用,整个事务的生命周期都在处理。
💡
final TransactionManager tm = determineTransactionManager(txAttr);
这里是获取事务管理器,Spring 支持两种事务管理器同步和Reactive类型

传播行为

  • 以非事务方式执行的
    • NOT_SUPPORTED 没有就非事务方式执行,有就直接挂起,然后非事务方式执行。
      NEVER 没有就非事务执行,有就抛出异常
  • 可有也可无事务方式执行的
    • SUPPORTS 有就用 没有则以非事务的方式执行的
      REQUIRED(默认方式)如果没有,就新建一个事务,如果有就加入当前事务,最常用的一种。MANDATORY 如果没有就抛出异常。如果有,就使用当前事务。
      REQUIRES_NEW 有没有都事务都会新建事务,如果原来有就将原来的事务挂起。该传播级别下,事务之间不会相互影响
      NESTED 如果没有,就新建一个事务;如果有,就再当前事务嵌套其他事务。
    • REQUIRES_NEW的区别
      • REQUIRES_NEW是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时(我们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。在NESTED情况下父事务回滚时,子事务也会回滚,而在REQUIRES_NEW情况下,原有事务回滚,不会影响新开启的事务。
    • REQUIRED的区别
      • REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于共用一个事务,所以无论调用方是否catch其异常,事务都会回滚 而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响
 

2. 事务传播行为的处理

前面提到的createTransactionIfNecessary 方法,最终调用的是 AbstractPlatformTransactionManager#getTransaction 这里面包含了事务信息的处理、开启事务,事务和线程的绑定、以及传播行为处理等逻辑。
这里的代码处理传播行为、获取数据库连接、开启事务、事务和线程绑定的逻辑等。
isExistingTransaction():如果目标对象的事务方法包含了其他代理的对象事务方法,在第二次调用该方法时会进入该分支。如下示例
事务的传播行为REQUIRED、REQUIRES_NEW、NESTED、NOT_SUPPORTED,这四种行为会引发挂起当前事务,用于和外层事务隔离。挂起的事务会被封装成SuspendedResourcesHolder实例存储在TransactionStatus 实例中在方法调用栈中进行传递。在新事务执行完毕时,会从TransactionStatus 中取出被挂起的事务继续执行。
notion image
事务的传播行为定义了是否开启事务、挂起和参与逻辑,如REQUIRES_NEW,作为独立事务存在,它是需要开启新事务,它会在新的数据库的连接中去完成自己的事务。事务的挂起是解绑当前的数据库连接,防止新事务重复用同一连接。

挂起事务和恢复事务

DataSourceTransactionObject 为当前事务的对象,它有一个ConnectionHolder对象属性,它维持着数据库连接信息。
挂起事务是将当前事务持有的连接置null,以及事务的资源解绑。
恢复事务就是使用之前挂起事务的SuspendedResourcesHolder 实例,将事务的状态恢复。之前挂起的事务将事务持有的 connection 置为null,但是恢复事务的时候并没有获取数据库连接与之绑定,只是将事务资源(数据库连接池工厂对象)绑定到事务。Spring 支持多个数据源场景下的事务管理。每个 DataSource 可以对应不同的数据库实例或连接池。使用 DataSource 作为资源进行绑定,可以区分不同数据源的事务上下文。同一线程内,可以通过不同的 DataSource 管理多个独立的事务资源。

2.开始事务

在前面的步骤以后,事务的传播行为、事务的上下文信息都已经处理完成后,接着就要准备获取数据库连接准备开始事务的操作。到这一步,事务的已经开启,根据事务的上下文信息构建一个 TransactionInfo实例,接着开始执行目标的事务方法。
notion image
从代理对象执行 invoke 方法开始,到这一步可以简单的总结为
  1. 构建事务上下文信息
    1. 事务的资源绑定
    2. 事务的基础信息处理
  1. 事务的传播行为处理
    1. 决定是否挂起事务
  1. 获取数据库连接
  1. 开启事务

3.事务的提交

执行目标事务方法以后,根据执行是否抛出异常,决定事务的提交和回滚,到这里事务方法的执行就结束了。

声明是事务失效的场景

1. 被调用的方法不是 public修饰的

在Bean初始化后,创建代理对象时,会判断Bean上的目标方法上提取@Transactional注解,构建TransactionAttribute 对象时,会判断目标方法的修饰符,如果是非 public 的是会构建当前方法的事务属性。

2. 方法用 final 或 static 修饰

如果事务方法用final修饰,将会导致事务失效。Spring 事务底层使用aop,也就是通过jdk动态代理或cglib,帮我们生成了代理类,在代理中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中 ,就无法重写该方法而添加事务功能。
同理如果某个方法是static的,同样无法通过动态代理,变成事务方法。

3. 对象没有被Spring 管理

2. 在不属于spring Bean管理的类中使用@Transactional
使用Spring事务的前提是:对象要被Spring的管理,需要创建bean实例。如果类没有加@Controller,
@Service @Component @ Repository 等注解,即该类没有交给Spring管理,那么它的方法也不会生成事务。

4. 表不支持事务

如果使用的 MySQL,只有 Inndb 支持事务。

5. 方法内部调用

示例说明: 在updateOrder方法上的事务会失效,因为发生了自身的调用,调用该类的自己的方法,而没有经过Spring的代理类,只有外部调用的事务才会生效。

如何解决该问题

  1. 再申明一个Service,由内部调用变成外部调用。
  1. 使用编程式事务
  1. 使用AopContext.currentProxy()方法获取代理对象
 

6. 未开启事务

如果是Spring项目,则需要手动配置事务相关的参数,如果忘记配置参数,则事务是不会生效的。
如果是SpringBoot项目,那么是不需要手动配置,因为SpringBoot已经在DataSourceTransactionManagerAutoConfiguration类中帮我们开启事务。

7. 吞掉异常

有些时候事务不会回滚,有可能是代码中手动catch了异常。开发者在在事务方法中catch了异常,也没有手动抛出,这种情况下Spring的事务是不会回滚的。
如果想要Spring能够正常回滚,必须抛出它能够处理的异常。

8.使用@Transactional方式错误

@Transactional注解的方法,默认是抛出RuntimeException,如果方法中抛出例如ParseException则无法处理。
Excetion
  • ---RuntimeException
  • ---ParseException
💡
该种情况引发事务失效的情况比较隐蔽,在第7点中提到如果想要Spring能够正常的回滚,必须抛出它能处理的异常Transactional中抛出RuntimeException,而ParseException不属于RuntimeException,所以事务无法正确回滚。
 
Prev
Spring源码阅读-AOP
Next
Session、Cookie、Token
Loading...