这几天给项目整合动态多数据源,遇到了事务和connection切换相关的问题,发现对Spring事务这块的代码没有仔细看过,就大体看下了Spring事务管理的实现
以下实现来自于sping-boot 2.6.4(spring-core 5.3.16)
这篇文章曾经发在掘金社区,是本人自己发布。
面向应用层的api
spring提供的操作事务的api:
- 声明式事务,比如最常用的@Transactional注解
- 使用TransactionManager、TransactionDefinition等类手动处理事务的开始/提交/回滚等操作
- 使用TransactionTemplate处理事务
其中,TransactionTemplate的使用比较简单,提供了execute()方法用于执行需要在事务进行的操作,对TransactionManager、TransactionDefinition等类的操作都封装在了TransactionTemplate内部
下面将对比手动处理事务和声明式事务的代码
使用JdbcTemplate手动操作事务
JdbcTemplate是spring封装的JDBC操作工具类。可以看到,JdbcTemplate依靠DataSourceUtils类来获取到事务关联的connection完成数据库操作。当用户代码中开启了事务时,JdbcTemplate使用的这一connection即事务关联的connection
1 | // JdbcTemplate类核心的execute函数 |
以下伪代码开启了传播级别为PROPAGATION_REQUIRED的事务,并实现了发生YourException异常时回滚事务
1 | // 使用 JdbcTemplate 手动处理事务操作的伪代码 |
使用注解@Transactional声明式处理事务
spring提供了@Transactional注解,spring内部自动通过AnnotationTransactionAttributeSource解析@Transactional注解上指定的事务属性;或者通过全局事务的方式,以NameMatchTransactionAttributeSource等方式定义interceptor的匹配规则,并为这些方法指定使用的TransactionAttribute,可全局配置事务
两种方式可以通过spring interceptor自动处理事务,具体的实现类为TransactionInterceptor——不过,这个类中只进行了基本的配置操作,为了方便扩展,aop操作事务的默认实现定义在了其父类TransactionAspectSupport中,核心的函数为:
1 | // 以下方法只保留了关键流程 |
不难发现,TransactionAspectSupport的处理流程与上文实现的使用JdbcTemplate手动处理事务操作的流程是一致的——spring通过aop的方式,为我们自动完成了事务的操作,使得只需要声明一个@Transactional注解就可以使用事务,为了实现与上文JdbcTemplate手动处理事务操作代码相同的效果,只需要:
1 |
|
多事务切换与传播机制的实现
使用面向应用的api,可以方便的使用事务。在上面的示例代码以及TransactionAspectSupport的核心处理代码中,只包含了当前的当个事务的处理逻辑,并不涉及spring提供的多个事务之间的传播机制和事务切换。这一部分已经由spring事务管理器做了封装。
事务管理有两套实现,比如核心的事务管理器:
- PlatformTransactionManager,命令式的实现(imperative)
- ReactiveTransactionManager,响应式的实现(reactive)
这里只看PlatformTransactionManager在单数据源下的实现
事务管理关键的类
事务管理器AbstractPlatformTransactionManager
Abstract base class that implements Spring’s standard transaction workflow, serving as basis for concrete platform transaction managers。是事务管理器的抽象实现,定义了核心的事务处理流程的函数接口,包括事务各状态处理的关键的函数,如begin(),commit(),rollback()
需要说明的是,这里提到的事务,应当区分出是逻辑上的事务还是真正关联到数据库connection的事务:
- 逻辑上的事务,这个称呼可能不太准确,应当称之为事务状态——比如,方法A、B都声明了开启事务;A中调用了B;A和B的事务隔离级别配置不同。那么,两个函数关联的事务状态是不同的,具有不同的事务属性,也可能关联到了不同的数据库事务资源。事务状态使用TransactionStatus类记录,由AbstractPlatformTransactionManager负责管理
- 实际的事务,数据库connection上开启的事务,关联到实际的数据库资源
AbstractPlatformTransactionManager只作为一个基础的模版类,负责管理事务状态(在AbstractPlatformTransactionManager类源码中完全没有出现connection)。作为实现事务传播机制的核心类,存在多个事务的切换时,能否切换、是否开启新事务、是否挂起/恢复、事务状态的保存,都已经由AbstractPlatformTransactionManager封装。
对于多个事务之间的切换,已经由AbstractPlatformTransactionManager完成了事务状态的处理,需要真正操作数据库事务资源的步骤,则留出函数,供子类面向不同的datasource扩展实现具体的事务资源(datasource,connection等)的管理和操作。比如,新建一个connection并操作connection开启事务(set autocommit = false);操作connection提交/回滚一个数据库事务
AbstractPlatformTransactionManager的子类:
- DataSourceTransactionManager,面向单个jdbc数据源的实现
- JtaTransactionManager,JTA的实现,即多数据源的分布式事务处理
事务状态TransactionStatus
事务状态的抽象接口,AbstractPlatformTransactionManager使用的事务状态类,基本实现为DefaultTransactionStatus,关键字段:
- Object transaction,事务对象,比如在DataSourceTransactionManager实现中使用transaction存储事务关联的数据库connection
- boolean newTransaction,当前是否是新开启的事务
- boolean newSynchronization,是否是新开始同步的事务
- boolean readOnly,事务是否是readonly的
- Object suspendedResources,记录当前事务之前挂起的事务对象,用于suspend后的resume恢复
其中关键的是suspendedResources,用于实现事务的suspend/resume
事务同步管理器TransactionSynchronizationManager
- 提供了基于threadlocal的事务状态、事务资源对象存储。将事务状态、事务资源绑定到threadlocal,从而在事务处理的过程中不需要考虑多线程之间的同步问题
- 可存储事务同步器TransactionSynchronization实例,其中提供了事务生命周期中部分状态的回调,比如:beforeCommit(boolean readOnly),afterCompletion(int status)
对于TransactionSynchronizationManager管理的事务资源connection,spring提供了工具类DataSourceUtils来操作
一个事务资源管理器的实现:DataSourceTransactionManager
管理事务关联的connection,提供了对单个JDBC数据源的事务资源的操作函数的实现
DataSourceTransactionManager对connection的管理中,会通过TransactionSynchronizationManager在启动事务时绑定一个connection,除非发生事务挂起/恢复,否则不会切换connection,而是一直使用同个connection
为什么AbstractPlatformTransactionManager封装了事务状态的切换,却不负责connection的切换?当然不能,AbstractPlatformTransactionManager是一个顶层的抽象模版,需要支持扩展出单数据源和多数据源,而多数据源的情况下显然不会是DataSourceTransactionManager中这样一直使用同个connection的情况
从事务创建流程看传播机制的处理
对于AbstractPlatformTransactionManager中对事务状态的处理流程,传播机制相关的处理主要是在创建/结束事务时,对应函数为getTransaction()/cleanupAfterCompletion(),抽取getTransaction()关键的部分给出了以下流程图:
事务的挂起与恢复
当A调用B,B调用C,并且A/B/C都声明开启事务时,那么涉及到多个事务的切换。在上一部分的事务创建流程可以看到,对不同的传播机制,大多是不需要开启新事务的,比如默认的REQUIRED,从始至终加入到同一个connection的同一个事务中,统一提交/回滚
而对REQUIERD_NEW的情况,要求每次开启一个新事务,那么对A->B->C的情况,就需要有一个栈来记录事务,每次开启新事务前,需要先suspend将原事务入栈;每次提交/回滚栈顶事务后,除非是最外层函数关联的事务,否则需要恢复到之前的事务继续进行处理
在事务状态层面,AbstractPlatformTransactionManager使用的TransactionStatus对象通过suspendedResources字段来实现对前一个事务的记录:每次开启一个新事务时,初始化一个TransactionStatus对象B,并将suspendedResources指向挂起的原TransactionStatus对象A;事务B处理完成后,从suspendedResources中恢复A,继续处理A。即通过suspendedResources属性,用前驱链表方便的实现了一个栈用于挂起-恢复
在上一部分的事务创建流程中记录了suspend原事务、启动新事务的过程,对应的可以在AbstractPlatformTransactionManager类的cleanupAfterCompletion()方法中看到,如果栈不为空,则会执行resume操作出栈前一事务继续处理
DataSourceTransactionManager对事务关联的connection的管理
对REQUIRED之类的传播属性,多个函数实际在同一个事务中统一提交/回滚。这种情况下,必须能够保证整个过程是使用的同一个connection
而对REQUIRE_NEW的情况,需要管理多个数据库事务,需要切换多个connection,那么同样的需要一个栈,来记录执行过程中每个事务关联的connection
在DataSourceTransactionManager中,借助TransactionSynchronizationManager提供的threadlocal存储,可以方便的保证线程执行中使用同一个connection,可以参看doBegin()方法:如果TransactionSynchronizationManager(threadlocal)中存储了connection对象,那么直接取用;否则才获取新的connection,以下摘取了关键代码:
1 |
|
而对需要切换connection的情况,那么则需要在父类AbstractPlatformTransactionManager通知切换事务——suspend/resume时,相应的完成connection的切换,如下:
1 |
|