史上最烂 spring transaction 原理分析
事务定义、事务作用、事务特性、生命周期、数据库事务三种运行模式、数据库事务控制、并发事务问题、隔离级别、数据库事务实现原理、spring 事务传播行为、spring 事务核心组件、spring boot 事务相关组件、事务嵌套原理、编程式事务与声明式事务、事务初始化过程、事务调用及执行过程、事务失效、事务与 java 锁、事务与多数据源。
版本
- jdk: 17
- spring: 6.1.3
- spring boot: 3.2.2
文章目录
- 史上最烂 spring transaction 原理分析
- 1 基本概念
- 1.1 事务定义
- 1.2 事务作用
- 1.3 事务特性(ACID)
- 2 数据库事务
- 2.1 事务生命周期
- 2.2 事务三种运行模式
- 2.3 事务控制
- 2.4 并发事务问题
- 2.5 隔离级别
- 2.6 实现原理
- 3 spring 事务隔离级别与传播行为
- 3.1 隔离级别
- 3.2 传播行为
- 4 核心组件
- 4.1 spring 相关组件
- 4.2 spring boot 相关组件
- 4.3 事务嵌套原理
- 5 使用方式
- 5.1 编程式事务
- 5.2 声明式事务
- 6 事务初始化
- 7 事务调用及过程
- 7.1 事务调用
- 7.2 事务过程
- 8 常见问题
- 8.1 事务失效
- 8.2 事务与 java 锁
- 8.3 事务与多数据源
1 基本概念
1.1 事务定义
事务是一个并发控制单位,控制用户定义的一个操作序列,这些操作要么全部完成,要么都不完成,是一个不可分割的工作单位。事务可确保数据库数据的一致性和完整性。
举个栗子,下单时需要创建订单和更新库存。从业务的角度理解,若成功创建订单则一定要减少库存,否则会出现超卖情况。换言之,这两个操作要么全部执行成功,要么都失败且数据恢复到执行之前的状态,二者是一个不可分割的工作单位。事务则可以保证其达到此目的。
1.2 事务作用
事务的作用主要包括以下几个方面:
- 保证数据的一致性:事务确保在一个操作序列中,这些操作要么全部完成,要么都不完成,以此保证数据库数据的一致性。
- 保证数据的完整性:事务通过 ACID 特性来保证数据的完整性,以防数据丢失或损坏。
1.3 事务特性(ACID)
- 原子性(Atomicity):一个事务中的所有操作要么全部完成,要么都不完成。若其中某个失败则所有操作都需回滚。
- 一致性(Consistency):整个事务执行过程中,数据库必须始终保持一致状态,即数据库的完整性不能被破坏。
- 隔离性(Isolation):多个事务之前是独立的,不能互相影响。数据库允许多个事务并发访问数据,隔离性可以防止事务交叉执行而导致数据不一致。事务隔离分为不同级别。
- 持久性(Durability):事务一旦成功提交,则对数据的修改是永久的,即便系统故障也不能丢失。
2 数据库事务
spring 事务本质上使用的是数据库事务,spring 事务再对数据库事务进行封装的基础上,增加了事务的传播行为,以便 spring 事务适应于更加广泛的业务场景。换言之,想要使用 spring 事务,其所选数据库则必须实现事务。故先以 mysql 为例简单介绍数据库事务。
2.1 事务生命周期
数据库事务的生命周期有 开启事务、执行事务、提交/回滚事务。
- 若事务操作执行成功则其生命周期为:开启事务、执行事务、提交事务。
- 若事务操作执行失败则其生命周期为:开启事务、执行事务、回滚事务。
2.2 事务三种运行模式
mysql 的事务有三种运行模式如下:
- 自动事务:隐式开启,隐士提交。即 mysql 默认为每条 sql 开启事务,在其执行完毕后自动执行 commit 进行提交。
- 隐士事务:隐式开启,显式提交。mysql 事务的自动提交 autommit 可以更改为手动提交,即在其执行完 sql 后需要手动执行 commit 进行提交。
- 显式事务:显式开启,显式提交。即需要手动 start transaction 开启事务,手动 commit 提交事务。
2.3 事务控制
事务控制中一个重要的机制是保存点(savepoint)(或可理解为 存档),其类似于虚拟机中的快照,主要用于回滚。在事务中每设置一个 savepoint 就是一个保存点,在事务未结束前可以回退到任意保存点,事务结束后则会自动删除该事务中的所有保存点。其在设计上类似于备忘录设计模式。在 jvm 指令中也有类似的机制,不过其称作安全点,其作用是 jvm 线程切换时使用,即记录下当前所执行代码的可暂停点,cpu 时间片结束后线程切换,当下次获得 cpu 时间片时接着从该暂停点开始执行。
事务控制流程如下:
- begin:即 start transaction,开启事务。
- savepoint:设置保存点,供将来回滚时使用。
- commit:即 提交,将当前事务所作出的修改持久化。
- rollback:回滚,取消当前事务所作出的修改。
- rollback to savepoint:回滚到保存点,即回退到存档点。
- release savepoint:删除所有保存点。
- set autocommit:将当前连接的提交模式重置为默认提交模式。
2.4 并发事务问题
事务并发执行时,若不加控制,则可能出现以下问题:
- 丢失更新:
- 第一类丢失更新:指当两个事务同时修改同一行数据时,其中一个事务回滚时会覆盖另一个已完成事务的更新结果。
- 第二类丢失更新:指当两个事务同时修改同一行数据时,其中一个事务的更新结果可能会被另一个事务覆盖。
- 脏读:指一个事务读取了另一个事务未提交的数据。此时若另一个事务回滚则第一个事务事务读取到的为无效数据(脏数据)。
- 幻读:指一个事务使用相同的条件进行两次查询操作,却得到不同的结果。这是因为两次查询之间有其它事务执行了更新操作。
- 不可重复读:指一个事务多次读取同一行记录,可能会得到不同的结果。这是因为期间其它事务可能会对其进行更新。
注:
- 脏读和不可重复读的区别:脏读是一个事务读取了另一个事务未提交的数据;不可重复读是一个事务读取了前一个事务已提交的数据。
- 幻读和不可重复读的区别:幻读是两次查询大量数据;不可重复读是两次查询同一行数据。
2.5 隔离级别
由于并发事务存在上述问题,故有了隔离级别的存在。
- READ_UNCOMMITED(读未提交):允许一个事务可以看到另一个事务未提交的数据。只能解决 第一类丢失更新 问题。
- READ_COMMITED(读并提交):保证一个事务更新的数据提交后才能被其它事务看到。解决了 第一类丢失更新 和 脏读 问题。
- REPEATABLE_READ(可重复读):保证同一个事务在相同条件下多次能获取到一致的数据。解决了 丢失更新、脏读 和 不可重复读 问题。
- SERIALIZABLE(串行化):强制事务串行化执行。解决了 脏读、幻读 和 不可重复读 问题。(实际开发中较少使用)。
其隔离级别依次由低到高,一般而言,隔离级别越高,数据安全性越高,但系统开销大,并发性能差。
2.6 实现原理
mysql 事务基于 InnoDB 存储引擎实现,其实现原理主要包括以下几个方面:
- redo log(重做日志):
- 作用:确保事务的持久性。即使在系统崩溃或故障后,已提交事务所做的更改也不会丢失。
- 内容:记录了事务对数据库所做的更改,即修改后的数据。
- 写入时机:通常在表中记录被修改前记录 redo log,以确保实际数据落盘前,日志能够被安全的记录。
- 适用场景:
- 数据库系统崩溃或故障重启后,可用其来恢复未落盘的数据。
- 数据库备份或恢复时,可用其来同步数据。
- 特点:顺序写入,保证数据完整性。
- undo log(撤销日志):
- 作用:确保事务的原子性,用于事务回滚。
- 内容:记录了事务对数据库所做的更改的逆操作,即修改前的数据。
- 写入时机:通常在表中记录被修改前记录 undo log,以确保事务回滚时撤销更改。
- 适用场景:
- 事务过程中,若需要回滚,则可用其来撤销事务对数据库的更改。
- MVCC(多并发版本控制)中,可用于提供数据的旧版本给其它事务读取。
- 特点:在事务结束后,其可能会被清除。
- MVCC(多版本并发控制):通过数据行的多个版本管理来实现数据库的并发控制,保证了在事务隔离级别下所读取数据的一致性。
- 锁机制:
- 共享锁(S Lock):允许事务读一行数据。
- 排它锁(X Lock):允许事务删除或更新一行数据。
- 事务提交/回滚:
- 提交:一旦事务提交,则会将已做修改永久落盘,并释放所有锁。
- 回滚:事务过程中若需要回滚,则回滚后已做修改将撤销,并释放所有锁。
3 spring 事务隔离级别与传播行为
3.1 隔离级别
spring 事务的隔离级别在数据库事务隔离级别的基础上增加了 DEFAULT(默认)级别。
- DEFAULT(默认) -1:即默认使用数据库事务的隔离级别(spring 事务默认使用该配置)。
- READ_UNCOMMITED(读未提交) 1:允许一个事务可以看到另一个事务未提交的数据。只能解决 第一类丢失更新 问题。
- READ_COMMITED(读并提交) 2:保证一个事务更新的数据提交后才能被其它事务看到。解决了 第一类丢失更新 和 脏读 问题。
- REPEATABLE_READ(可重复读) 4:保证同一个事务在相同条件下多次能获取到一致的数据。解决了 丢失更新、脏读 和 不可重复读 问题。
- SERIALIZABLE(串行化) 8:强制事务串行化执行。解决了 脏读、幻读 和 不可重复读 问题。(实际开发中较少使用,因为耗费性能)。
3.2 传播行为
事务的传播行为一般出现在事务嵌套的场景中。如一个事务方法中调用了另一个事务方法,那么这两个事务方法该如何开启和提交等等。此时则由配置的事务传播机制来决定。换言之,事务的传播机制回答了这样一个问题: 一个事务应该被执行还是被挂起,或者说一个方法是否应该在事务性的程序中执行。
- REQUIRED 0:如果外层有事务,则将当前事务加入外层事务,一同提交,一同回滚。如果外层没有事务,则新建一个事务执行。
- SUPPORTS 1:如果外层有事务,则加入外层事务。如果外层没有事务,则以非事务的方式执行。
- MANDATORY 2:如果外层没有事务则发生异常。
- REQUIRED_NEW 3:每次都会开启一个新事务,如果外层有事务则将其挂起,待当前事务执行完成后在执行外层事务。如果外层没有事务则创建一个新事务执行。
- NOT_SUPPORTED 4:不支持事务,如果外层有事务则将其挂起,待当前代码执行完成后再恢复外层事务的执行,无论当前代码执行是否成功都不会发生回滚。
- NEVER 5:如果外层有事务则发生异常。
- NESTED 6:如果外层有事务,则创建一个新的嵌套事务;若外层没有事务,则创建一个新的事务(等价于 REQUIRED)。
4 核心组件
spring 事务相关核心组件由 spring(spring-tx)提供,spring boot 诞生后,在 spring-boot-autoconfigure(自动配置)包中实现了 spring 事务的自动配置。注:下文主要围绕数据库事务进行分析。
4.1 spring 相关组件
spring 提供了事务开启、事务管理、事务定义、事务状态、事务解析初始化及事务调用等组件,其中相关核心组件如下:
-
1. @EnableTransactionManagement
开启 spring 事务管理功能,以便使用 @Transactional 注解声明事务。即若是 spring 项目则必须添加该注解,才可以使用声明式事务。spring boot 对 spring 事务提供了自动配置,故在 spring boot 项目中无需手动开启事务即可使用声明式事务。
-
2. PlatformTransactionManager
public interface PlatformTransactionManager extends TransactionManager {// 获取事务TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;// 提交事务void commit(TransactionStatus status) throws TransactionException;// 回滚事务void rollback(TransactionStatus status) throws TransactionException; }
平台事务管理器接口,其定义了三个方法分别是:获取事务、提交事务和回滚事务。因为 spring 事务底层依赖于数据库事务,故其封装了操作事务的具体行为,然后再交由具体的事务平台去实现。其主要实现类如下:
- DataSourceTransactionManager:管理基于数据源(Jdbc)事务的管理器。spring boot 在自动配置事务时,默认使用该类的子类 JdbcTransactionManager。该类通过聚合数据源连接 Connection 来操作数据库事务。
- JtaTransactionManager:管理基于 Jta 分布式事务的管理器。
- KafkaTransactionManager:管理基于 kafka 事务的管理器。
- ResourcelessTransactionManager:管理无资源事务的管理器。无资源事务指不需要与外部资源(如数据源)交互的事务,属于本地事务。该管理器由 spring batch 实现。
-
3. TransactionSynchronizationManager
事务同步管理器,主要有两个作用,一是以线程为单位存储了事务过程中的各种信息,如数据源连接、事务状态等;二是聚合了 TransactionSynchronization,后者的主要功能是为事务过程的各个阶段提供回调方法,这些方法在事务过程中(如开启、回滚和提交)会被回调,开发者可通过实现该接口来添加回调的业务逻辑。
-
4. TransactionDefinition
事务定义信息,其封装了事务的一些基本属性,如隔离级别、传播行为、回滚规则、是否只读、事务超时等。一般情况下我们会使用 @Transactional 注解来配置事务,该注解在被解析时会将解析结果封装成 TransactionDefinition 的子类 TransactionAttribute 对象。
-
5. TransactionStatus
事务状态信息,其定义了事务状态相关行为,如是否为新事物、是否有安全点、是否为只回滚、是否已完成等。事务执行过程中,通过 PlatformTransactionManager#getTransaction() 获取事务时,会根据事务定义信息 TransactionDefinition 来构建事务状态信息 TransactionStatus。DefaultTransactionStatus 是其主要实现类。
-
6. @Transactional
声明式事务注解,什么作用就不用说了吧!被该注解标注的方法在容器启动时(具体是在 bean 后置处理器即 BeanPostProcessor#postProcessAfterInitialization 方法执行时,即在 bean 初始化之后增强时)会被 SpringTransactionAnnotationParser(spring 事务注解解析器)解析成 TransactionAttribute 对象维护在 TransactionAttributeSource 类的 map 集合中(实际上该 map 维护在其子类 AbstractFallbackTransactionAttributeSource 中),其 key 为事务方法唯一标识,value 事务属性源,即切点。具体解析过程,会在事务初始化章节进行详解。
-
7. TransactionAnnotationParser
事务注解解析器,主要实现类为 SpringTransactionAnnotationParser(spring 事务注解解析器),其主要作用是解析用 @Transactional 注解声明的事务方法,将解析结果封装成 TransactionAttribute 对象。同时还提供了 Jta 事务、Ejb3 事务等解析器。
-
8. TransactionAttributeSource
事务属性源,即事务来源(如被 @Transactional 注解所标注的方法),如声明式事务、编程式事务等。其本质是一个 aop 切点。
其维护了事务注解解析器集合,即 Set< TransactionAnnotationParser> annotationParsers,用来解析注解事务。在实例化该 bean 时,会将 SpringTransactionAnnotationParser 解析器添加到该集合中。
其维护了一个 Map<Object, TransactionAttribue> attributeCache 集合,用来存储事务解析器 TransactionAnnotationParser 解析的结果。其主要实现类为 AnnotationTransactionAttributeSource(默认使用该实现类),同时还提供了 CompositeTransactionAttributeSource 和 MethodMapTransactionAttributeSource 等实现类。
上图为 spring transaction 与 spring aop 关系图,着重描述了 aop 切面 = 切点 + 通知 与 spring 事务的关系。
-
1. TransactionAttributeSource
spring 事务属性源,其被持有在事务属性源切点类 TransactionAttributeSourcePointcut 内,故其本质是一个 aop 切点,其内部持有一个事务注解解析器列表,用来解析 @Transactional 注解所标注的方法,因此,被 @Transactional 注解标注的方法就是事务 aop 的切点。
-
2. TransactionInterceptor
事务拦截器,其实现了 aop 环绕通知接口 MethodInterceptor,且继承了 TransactionAspectSupport 类,故其本质是一个 aop 通知,其通知内容为事务的创建、回滚和提交等。其以适配器的方式调用了 TransactionAspectSupport#invokeWithinTransaction 方法。这个方法才是 spring 事务 aop 通知的具体实现。
-
3. TransactionAspectSupport
事务方法被调用的具体实现,即事务 aop 通知的具体实现。其核心伪代码如下:
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {// 获取事务属性源 即 ProxyTransactionManagementConfiguration 配置中配置的事务 aop 切点TransactionAttributeSource tas = getTransactionAttributeSource();// 根据指定的切点 tas 以及目标类和目标方法获取事务属性(即 从 attributeCache map 集合中获取)final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);// 根据事务属性推断事务管理器// 若是 spring 环境 则需要手动向容器 中注册事务管理器 bean// 若是 spring boot 环境 则 DataSourceTransactionManagerAutoConfiguration 会自动注册事务管理器 beanfinal TransactionManager tm = determineTransactionManager(txAttr);// 将事务管理器对象强转为父类类型PlatformTransactionManagerPlatformTransactionManager ptm = asPlatformTransactionManager(tm);final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {// 创建事务 此处会应用事务传播行为 如创建新的事务亦或是加入当前事务 同时会将事务与数据源连接 Connection 绑定TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);Object retVal;try {// 调用目标方法retVal = invocation.proceedWithInvocation();}catch (Throwable ex) {// 目标方法抛出异常后 完成事务(回滚或不回滚)completeTransactionAfterThrowing(txInfo, ex);throw ex;}finally {// 清除事务信息 因为 spring 事务是基于 ThreadLocal 来做的 故在事务结束后(提交或回滚)都要将当前事务信息清除cleanupTransactionInfo(txInfo);}// 目标方法返回后提交事务commitTransactionAfterReturning(txInfo);return retVal;} }
-
4. BeanFactoryTransactionAttributeSourceAdvisor
其实现了 aop Advisor 接口,故其本质是一个 aop 切面,切面 = 切点 + 通知,TransactionAttributeSource 是其切点,TransactionInterceptor 是其通知。不要纠结 TransactionAttributeSourcePointcut 与 TransactionAttributeSource 和 TransactionInterceptor 与 TransactionAspectSupport 这两对之间到底谁是名义上的切点和通知,不重要,重要的你要知道具体谁实现了切点和通知的功能。
-
5. ProxyTransactionManagementConfiguration
代理事务管理配置,配置类,其配置了 spring 事务 aop 切面、切点和切面三个 bean,即 BeanFactoryTransactionAttributeSourceAdvisor、TransactionAttributeSource 和 TransactionInterceptor。(其中在配置 TransactionAttributeSource 时会创建一个 SpringAtransactionAnnotationParser 解析器添加到 TransactionAttributeSource 的 annotationParsers 集合中)。
-
6. AutoProxyRegister
自动代理注册器,本质是一个 BPP,其作用是为事务方法所在类(即被 @Transactional 注解所标注的方法所在类)创建代理对象,并将其注册到容器中。对于文中出现的 aop 相关的知识点的详解(如切面、切点、通知和代理创建等)请移步 [史上最烂 spring aop 原理分析](史上最烂 spring aop 原理分析-CSDN博客)。
-
7. @EnableTransactionManagement
上文已说明其作用是开启 spring 事务管理功能,其具体原理是通过 @Import 引入 TransactionManagementConfigurationSelector,该类想容器中注册了 AutoProxyRegistrar bean 和 ProxyTransactionManagementConfiguration bean,以此来达到开启事务的目的。
4.2 spring boot 相关组件
spring boot 为整合 spring 事务,为其提供了两个自动配置类,即 DataSourceTransactionManagerAutoConfiguration 和 TransactionAutoConfiguration,分别是数据源事务自动配置类和 spring 事务配置类。
-
1. DataSourceTransactionManagerAutoConfiguration
即 spring boot 提供的关于数据源事务的自动配置类。其生效条件是程序中存在 DataSource 类和 JdbcTemplate 类。其会基于 DataSource 和 JdbcTemplate bean 向容器中注册 DataSourceTransactionManager bean,且其实际上注入的是 JdbcTransactionManager bean。
-
2. TransactionAutoConfiguration
即 spring boot 提供的关于 spring 事务的自动配置类,其依赖于上述配置的结果 DataSourceTransactionManager bean。该配置类提供了三个 bean 和一个配置类,分别是:
-
TransactionManagerCustomizers:
即事务管理器自定义扩展器,其作用是对事务管理器提供一些自定义扩展处理。当容器不存在 TransactionManagerCustomizers bean 才会注入该 bean。
-
TransactionOperator:
即响应式事务,其作用是提供响应式事务。若为响应式编程,则应该使用响应式事务管理器管理事务。当容器中存在 ReactiveTransactionManager bean 时才会注入该 bean。
-
TransactionTemplate:
即编程式事务,其作用是提供编程式事务。当容器中存在 PlatformTransactionManager bean 时才会注入该 bean。
-
EnableTransactionManagementConfiguration:
即开启 spring 事务管理配置的配置类。spring 事务是基于 spring aop 实现的,所以该自动配置的本质是配置 spring 事务 aop,如使用哪种代理方式(jdk or cglib),如何定义切面、切点、通知及代理创建等。而其具体则是直接使用了 spring 提供的 @EnableTransactionManagement 注解。此外,该配置类内部还提供了两个配置,分别是:
- 基于接口的 jdk 动态代理的事务配置。
- 基于子类的 cglib 动态代理的事务配置,默认使用 cglib 动态代理的事务配置。
-
注:若提供了自定义实现的 DataSourceTransactionManager 或则引导类上标注了 @EnableTransactionManagement 注解,则以自定义配置为准。
4.3 事务嵌套原理
spring 事务是支持嵌套的,如上图所示,在事务 A 的过程中开启了事务 B,在事务 B 的过程中又开启了事务 C,事务 C 结束后又回到了事务 B,事务 B 结束后又回到了事务 A,直到事务 A 结束。即在事务 A 的生命周期里包含了事务 B,在事务 B 的生命周期里又包含了事务 C。
由上图可知,事务 A 先最先开启但最后结束,事务 B 第二开启第二结束,事务 C 最后开启最早结束,显然,这符合数据结构 栈 的特点,当然,spring 也是这样设计的。TransactionAspectSupport 类内部定义了一个不可被修改的静态类 TransactionInfo,用来存储事务嵌套信息,其核心代码如下:
protected static final class TransactionInfo {@Nullable // 当前事务所属的事务管理器private final PlatformTransactionManager transactionManager;@Nullable // 当前事务属性private final TransactionAttribute transactionAttribute;@Nullable // 当前事务状态private TransactionStatus transactionStatus;@Nullable // 旧事务信息(放在栈中 那就是上一个入栈的事务信息)private TransactionInfo oldTransactionInfo;// 将新事务绑定到当前线程private void bindToThread() {this.oldTransactionInfo = transactionInfoHolder.get(); // 即然有了新事务 那么栈顶的就成了旧事务transactionInfoHolder.set(this); // 新事务则放置栈顶}// 重置当前线程事务信息(源码中有这么一行注释:Use stack to restore old transaction TransactionInfo.)private void restoreThreadLocalStatus() {transactionInfoHolder.set(this.oldTransactionInfo); // 当栈顶的事务完成后 则将栈中下一个事务置为当前事务(若无下一个则为 null)}
}
同时,TransactionAspectSupport 声明了一个 ThreadLocal< TransactionInfo> transactionInfoHolder 对象,来存储每个线程中的事务栈。
5 使用方式
spring 事务共提供了两种事务方式,即编程式事务和声明式事务,下述伪代码以 spring boot 项目为背景示例,若是 spring 项目,则记得加上 @EnableTransactionManagement 注解哦。
5.1 编程式事务
@Autowired // 注入事务管理器
private PlatformTransactionManager transactionManager;public void test() {// 创建事务 DefaultTransactionDefinition 中可以设置事务隔离级别、传播行为和回滚异常等属性TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());try {// 业务代码transactionManager.commit(status); // 提交事务} catch (Exception e) {transactionManager.rollback(status); // 业务代码异常后回滚事务}
}
5.2 声明式事务
// 注解属性中可设置隔离级别、传播行为和回滚异常等
@Transactional(rollbackFor = OneException.class, noRollbackFor = OneException.class, propagation = Propagation.REQUIRES_NEW)
public void test() {// 业务代码
}
6 事务初始化
如上图所示,为 spring 事务初始化时序图(spring boot 背景下),简要说明如下(图中即下文说明涉及到 spring ioc 容器启动过程,其详细说明见 [史上最烂 spring ioc 原理分析](史上最烂 spring ioc 原理分析-CSDN博客)):
- 1、ConfigurableApplicationContext#refresh():众所周知,spring ioc 是 spring 系列的核心,而 refresh() 方法则是 spring ioc 的核心及入口,当然,在 spring boot 中也不例外,spring boot 引导类的 main() 方法最终会调用 refresh() 方法来启动容器,所以,故事的一切,又从这里开始了。
- 1.1、AbstractApplicationContext#finishBeanFactoryInitialization():refresh() 方法的第 11 步,这一步的主要作用是在容器初始化完成后,完成剩余非懒加载单例 bean 的创建。即通过 beanDefinitaionMap 中的 bean 定义来初始化 bean。
- 1.1.1、ConfigurableListableBeanFactory#preInstantiateSingletons():上一步具体是调用该方法来创建非懒加载的单例 bean。
- 1.1.1.1、BeanFactory#getBean():首先,bean 的生命周期为:实例化、属性设置、初始化、使用和销毁,在 getBean() 方法执行过程中,会以此经历实例化、属性设置和初始化。其中在 属性设置前 和 初始化后 会进行代理创建,具体为当发生循环依赖时在属性设置(依赖注入)前创建代理,未发生循环依赖时在初始化后创建代理。
- 1.1.1.1.1、AbstractAutoProxyCreator#postProcessAfterInitialization():该方法作用是在 bean 初始化后对 bean 进行增强,其定义在 BeanPostProcessor 即 bean 后置处理器中,AbstractAutoProxyCreator 类实现了该方法,用来为 bean 创建代理对象。
- 1.1.1.1.1、AbstractAutoProxyCreator#getAdvicesAndAdvisorForBean():获取当前 bean 相关的所有通知和切面(即增强)。
- 1.1.1.1.1.1、TransactionAttributeSource#getTransactionAttribute():获取事务属性,声明式事务即 @Transactional 其本质也是利用 aop 对方法增强,所以在上一步过程中会进入该方法(TransactionAttributeSource 在 aop 中扮演了切点的角色)。然后会逐个调用 attributeParsers 集合中的解析器(一般情况下只有 SpringTransactionAnnotationParser 一个),以此来获取事务属性,且会将结果封装成 TransactionAttribute 对象存储在 attributeCache 集合中,以便在事务过程中使用。
- 1.1.1.1.2、CglibAopProxy#getProxy():使用 cglib 创建代理对象。
- 1.1.1.1.1、AbstractAutoProxyCreator#getAdvicesAndAdvisorForBean():获取当前 bean 相关的所有通知和切面(即增强)。
- 1.1.1.1.1、AbstractAutoProxyCreator#postProcessAfterInitialization():该方法作用是在 bean 初始化后对 bean 进行增强,其定义在 BeanPostProcessor 即 bean 后置处理器中,AbstractAutoProxyCreator 类实现了该方法,用来为 bean 创建代理对象。
- 1.1.1.1、BeanFactory#getBean():首先,bean 的生命周期为:实例化、属性设置、初始化、使用和销毁,在 getBean() 方法执行过程中,会以此经历实例化、属性设置和初始化。其中在 属性设置前 和 初始化后 会进行代理创建,具体为当发生循环依赖时在属性设置(依赖注入)前创建代理,未发生循环依赖时在初始化后创建代理。
- 1.1.1、ConfigurableListableBeanFactory#preInstantiateSingletons():上一步具体是调用该方法来创建非懒加载的单例 bean。
- 1.1、AbstractApplicationContext#finishBeanFactoryInitialization():refresh() 方法的第 11 步,这一步的主要作用是在容器初始化完成后,完成剩余非懒加载单例 bean 的创建。即通过 beanDefinitaionMap 中的 bean 定义来初始化 bean。
7 事务调用及过程
7.1 事务调用
事务调用即 aop 事务增强通知方法的调用,该方法由环绕通知接口 org.aopalliance.intercept.MethodInterceptor 定义,由 aop 事务通知类 TransactionInterceptor 实现,即 TransactionInterceptor#invoke() 方法。
7.2 事务过程
事务过程即 spring 事务对目标方法做出的增强,即 aop 通知内容。通知内容实现在 TransactionAspectSupport#invokeWithinTransaction() 方法内,TransactionAspectSupport 实例被 TransactionInterceptor 聚合,以此来执行通知内容。为了加强记忆,再贴出该方法伪代码:
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {// 获取事务属性源 即 ProxyTransactionManagementConfiguration 配置中配置的事务 aop 切点TransactionAttributeSource tas = getTransactionAttributeSource();// 根据指定的切点 tas 以及目标类和目标方法获取事务属性(即 从 attributeCache map 集合中获取)final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);// 根据事务属性推断事务管理器// 若是 spring 环境 则需要手动向容器 中注册事务管理器 bean// 若是 spring boot 环境 则 DataSourceTransactionManagerAutoConfiguration 会自动注册事务管理器 beanfinal TransactionManager tm = determineTransactionManager(txAttr);// 将事务管理器对象强转为父类类型PlatformTransactionManagerPlatformTransactionManager ptm = asPlatformTransactionManager(tm);final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {// 创建事务 此处会应用事务传播行为 如创建新的事务亦或是加入当前事务 同时会将事务与数据源连接 Connection 绑定TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);Object retVal;try {// 调用目标方法retVal = invocation.proceedWithInvocation();}catch (Throwable ex) {// 目标方法抛出异常后 完成事务(回滚或不回滚)completeTransactionAfterThrowing(txInfo, ex);throw ex;}finally {// 清除事务信息cleanupTransactionInfo(txInfo);}// 目标方法返回后提交事务commitTransactionAfterReturning(txInfo);return retVal;}
}
8 常见问题
事务实际上为业务逻辑的增强,即它与业务逻辑是耦合的,因此,在不同场景下的事务使用中可能会出现一些问题,如事务失效、事务与锁、事务与多数据源等。以下以声明式事务即 @Transactional 为背景说明:
8.1 事务失效
spring 事务常见的八种失效场景
- 1、异常被捕获或捕获后未抛出:若事务方法(即被 @Transactional 注解标注的方法)中的异常被捕获或捕获后未抛出,则事务失效。由 TransactionAspectSupport#invokeWithinTransaction() 方法内容可知,只有事务方法调用过程中抛出异常时,才会执行回滚(即 TransactionAspectSupport#completeTransactionAfterThrowing())。故在事务方法中,要么不捕获异常;要么捕获后做出相应业务逻辑后,再抛出异常。
- 2、未指定回滚异常:spring 事务默认回滚运行时异常(RuntimeException),若事务方法可能会产生或抛出自定义异常(特指直接继承了 Exception 的自定义异常),则必须指定回滚异常(即设置属性 rollbackFor 为自定义异常)。
- 3、目标类未被 ioc 容器管理:若事务方法所在类未被 ioc 容器管理(即目标类未被 @Service 等组件注解标注),则事务失效。因为只有目标类会被注册进 ioc 容器时,在容器初始化过程中才会为目标方法进行 aop 增强。
- 4、目标方法不能被实现:jdk 动态代理的要求是目标类必须实现一个或多个接口,换言之,jdk 动态代理是基于接口实现的(亦或是基于方法实现的)。故当目标方法不能被实现时(如 static 方法、final 方法、private 方法等),事务会失效。
- 5、目标方法不能被重写:cglib 动态代理是基于继承实现的,即其是通过继承目标类来生成一个代理类。所以目标类中的成员变量、static 方法、final 方法、private 方法是不能被其代理的。也可以理解为 cglib 动态代理是基于方法重写的。
- 6、方法内部直接调用:若在目标类的方法 A(未被 @Transactional 标注) 中直接调用目标类的方法 B(被 @Transactional 标注),则事务失效。因为此事务方法为 B,但入口却为 A,但 A 未被 @Transactional 标注,故不会被 aop 增强(即不会被织入事务过程代码)。(其实这里还有另一个说法,即事务方法必须被其所在类的 bean 调用)。
- 7、使用了错误的传播机制:假设事务方法 A 中调用了事务方法 B,且我们希望二者同时回滚,同时提交,此时,若传播机制为 REQUIRED_NEW 时,事务失效。因为当嵌套事务同时回滚,同时提交时,二者必然为同一个事务,即 B 事务加入了 A 事务,而 REQUIRED_NEW 的作用是每次都会开启一个新事务。
- 8、异步多线程:若在事务方法 A 中调用了异步方法 B(如 B 被 @Async 标注),则 A 方法中的业务逻辑会被事务作用,B 方法则失效。因为 @Async 注解使用的是独立线程和独立事务,不在主线程(调用 A 方法的线程)的事务(事务 A)内,故失效。换言之,多线程中,每个线程都有自己的事务,不能共享。
- 9、数据库不支持事务:spring 事务底层是依赖于数据库事务的,若数据库不支持事务,则失效。
8.2 事务与 java 锁
spring 事务与 java 锁又会擦出怎样的火花呢,请看这个场景:我们还以下订单减库存为例,显然这是一个原子性操作,需要加事务,同时,若在高并发场景下(很明显高并发下订单减库存场景很常见)要保证数据安全,则必须要加锁。我们以此为例,那么伪代码大概长这样:
private final Lock lock = new ReentrantLock();@Transactional
@Override
public void zed() {try {lock.lock();// 业务逻辑 需要保持原子性} finally {lock.unlock();}
}
针对上述业务场景,通常有以下解法(当然我相信聪明的你肯定能研究出更多的解法 欢迎补充):
-
1、根据上述伪代码,再结合事务逻辑,那么显然,会出现超卖情况,即锁失效。因为在事务提交前(数据落盘前)锁已经释放了。故,解决方案当然是将事务方法放在锁的作用内即可。那么正确的伪代码大概长这样:
private final Lock lock = new ReentrantLock();private final IZedService zedService; // 构造注入@Override public void fizz() {try {lock.lock();this.zedService.ahri();} finally {lock.unlock();} }@Transactional @Override public void ahri() {// 业务逻辑 需要保持原子性 }
-
2、事务串行序列化,那么伪代码长这样:
@Transactional(isolation = Isolation.SERIALIZABLE) @Override public void zed() {// 业务逻辑 需要保持原子性 }
事务换行化,甚至都不用加锁,但是!它耗费性能啊,所以不推荐,除非你的场景对性能要求不高。
-
3、数据库行锁-排它锁,即 for update 语句。
8.3 事务与多数据源
众所周知,一般情况下 spring 事务是不支持单元数据源的,所以在多数据源场景下,有以下几种方案可用(当然 我相信聪明的你肯定能想到其它方案 望分享):
-
1、若使用了 mybatis-plus,则可以直接使用 @DSTransactional 注解来代替 @Transactional。
-
2、REQUIRES_NEW 隔离级别:每次都创建新的事务,则嵌套事务对应数据源连接是相互独立的。
-
3、自定义事务管理器:即为每个数据源定义一个事务管理器,并在使用 @Transactional 注解时通过 value 属性设置其对应数据源的事务管理器即可。
简单提供个栗子,其大致思路为:自定义数据源当前上下文持有器 DbContextHolder,用来存储每个线程当前上下文对应数据源;重写 spring jdbc 路由数据源 AbstractRoutingDataSource#determineCurrentLookupKey() 方法,使其从 DbContextHolder 持有器中获取数据源;定义多数据源配置 MultipleDatasourceConfig;定义多数据源事务管理器配置 MultipleDatasourceTransactionManager。示例代码如下:
public class DbContextHolder {private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();private static final String DEFAULT_DATABASE = "master";public static String getDbType() {String datasource;if ((datasource = HOLDER.get()) == null) {return DEFAULT_DATABASE;}return datasource;}public static void setDbType(String datasource) {HOLDER.set(datasource);}public static void clearDbType() {HOLDER.remove();} }
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return DbContextHolder.getDbType();} }
@Configuration public class MultipleDatasourceConfig {@Bean // 主数据源public DataSource master() {HikariDataSource dataSource = new HikariDataSource();dataSource.setJdbcUrl("jdbc:mysql://****:****/****?useUnicode=true&characterEncoding=utf8&useSSL=false&&serverTimezone=UTC");dataSource.setUsername("****");dataSource.setPassword("******");dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");return dataSource;}@Bean // 从数据源public DataSource slaveOne() {HikariDataSource dataSource = new HikariDataSource();dataSource.setJdbcUrl("jdbc:mysql://****:****/****?useUnicode=true&characterEncoding=utf8&useSSL=false&&serverTimezone=UTC");dataSource.setUsername("****");dataSource.setPassword("******");dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");return dataSource;}@Bean // @Primary 注解必须有 因为该数据源为真正数据源 @Qualifier 也必须有 因为 DataSource bean 可能有多个候选者@Primarypublic DataSource multipleDatasource(@Qualifier("master") DataSource master,@Qualifier("slaveOne") DataSource slaveOne,@Qualifier("slaveTwo") DataSource slaveTwo) {DynamicRoutingDataSource multipleDatasource = new DynamicRoutingDataSource();Map<Object, Object> map = new HashMap<>();map.put("master", master);map.put("slaveOne", slaveOne);map.put("slaveTwo", slaveTwo);multipleDatasource.setTargetDataSources(map);multipleDatasource.setDefaultTargetDataSource(master);return multipleDatasource;}@Bean // 若为 mybatis/mybatis-plus 则需配置该 bean 当然还需要 SqlSessionTemplate bean(偷个懒 略掉了)public SqlSessionFactory sqlSessionFactory(@Qualifier("multipleDatasource") DataSource multipleDatasource) throws Exception {MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();factoryBean.setDataSource(multipleDatasource);// 此处还可配置 MybatisConfigurationreturn factoryBean.getObject();} }
@Configuration public class MultipleDatasourceTransactionManager {@Beanpublic PlatformTransactionManager transactionManager(@Qualifier("master") DataSource master) {return new DataSourceTransactionManager(master);}@Beanpublic PlatformTransactionManager transactionManagerOne(@Qualifier("slaveOne") DataSource slaveOne) {return new DataSourceTransactionManager(slaveOne);} }
@Transactional(value = "transactionManager") // 为事务指定事务管理器 public void test() {// 业务逻辑 }