Spring事务源码解析
公众号:完美的工程学
gitee:https://gitee.com/duchenxi/total-war
点关注不迷路!
1. 一个简单的demo
通过一个简单的demo让我们了解spring事务的基本使用方式。
事务配置类
package com.ducx.playground.spring.transaction;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
@Bean("basicDataSource")
public BasicDataSource basicDataSource(){
BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setDriverClassName("com.mysql.jdbc.Driver");
basicDataSource.setUrl("jdbc:mysql://xxx.xxx.xxx.xxx:3306/test");
basicDataSource.setUsername("root");
basicDataSource.setPassword("111111");
basicDataSource.setMaxTotal(1);
return basicDataSource;
}
@Bean("jdbcTemplate")
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
@Bean("transactionManager")
public TransactionManager transactionManager(DataSource dataSource){
TransactionManager transactionManager = new DataSourceTransactionManager(dataSource);
return transactionManager;
}
}
业务接口
package com.ducx.playground.spring.transaction;
public interface UserService {
void saveUserInfo() throws Exception;
void block() throws Exception;
}
业务接口的实现类
package com.ducx.playground.spring.transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private UserService userService;
@Transactional(transactionManager = "transactionManager", propagation = Propagation.SUPPORTS)
public void saveUserInfo() throws Exception{
jdbcTemplate.execute("insert into test values ('杜晨曦11');");
}
@Transactional(transactionManager = "transactionManager", propagation = Propagation.REQUIRES_NEW)
public void block() throws Exception{
jdbcTemplate.execute("insert into test values ('杜晨曦12');");
userService.saveUserInfo();
jdbcTemplate.execute("insert into test values ('杜晨曦12');");
}
}
测试类
package com.ducx.playground.spring.transaction;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class TransactionClient {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext("com.ducx.playground.spring.transaction");
UserService user = (UserService) context.getBean("userServiceImpl");
user.block();
}
}
2.基本概念描述
在进行事务的实际使用和源码解析之间我们先来了解一下spring事务里面的基本的概念。下面介绍的这些参数都是在我们的使用过程中在注解@Transactional中可能需要被配置的,这些被称为注解的元数据配置,下面我们一起来看一下这些基本的配置参数的概念(常用的)。
2.1事务传播行为
首先我们介绍一下最关键的参数propagation,这个参数的意思是传播行为,传播行为指的就是在程序的调用链路中如果执行的方法链路中不止一个方法用@Transactional修饰了,那么针对于调用方法和被调用方法他们之间的事务是怎么被处理的。
关于事务传播行为在接口TransactionDefinition里面有着详细的定义以及解释。在spring事务中一共定义了其中事务的传播行为:
- Required:支持当前存在的事务,如果当前不存在事务的话就创建一个事务,这也是默认的事务传播行为。
- Supports:支持当前事务,但是如果当前没有事务的话就不创建事务通过没有事务的形式执行。
- Mandatory:支持当前事务,如果当前没有事务存在的话就抛出异常。
- Requires_New:不管当前有没有事务都创建一个事务来执行,并且如果当前存在事务的话就挂起当前的事务。
- Not_Supportes:不支持当前事务,总是以没有事务的形式来执行。
- Never:不支持当前事务,如果当前存在事务的话就抛出异常。
- Nested:如果当前存在事务的话通过嵌套式事务的方式来执行。
2.2 事务隔离级别
针对于数据库的事务隔离界别spring事务在执行的时候也可以按照需求被指定,同样的在TransactionDefinition接口里面也定义了5种事务的隔离级别等级,为什么是5种?因为@Transactional默认的事务隔离级别是ISOLATION_DEFAULT表示使用数据库配置的隔离界别,比如mysql默认的隔离级别就是可重复读。通过这个接口的信息来总结一下4种事务隔离级别:
- READ_UNCOMMITTED:读未提交,可能会存在脏读的情况,脏读就是当前事务读取到别的事务没有提交的数据,这种隔离级别存在着很大的数据安全隐患。
- READ_COMMITTED:读已提交,解决了脏读的问题,但是会存在幻读的问题,幻读就是当前事务能够读取到别的事务提交的数据导致同一个事务中两个相同的查询语句可能会读取到不同的数据行数,Oracle的默认事务隔离界别就是读已提交,这个相对于读未提交数据的安全性提高了不少。
- REPEATABLE_READ:可重复读,解决了幻读的问题,也是mysql数据库的默认隔离级别,innoDB引擎通过多版本并发控制(MVCC)的方式来解决了幻读的问题。
- SERIALIZABLE:串行化,事务隔离级别的最高等级,所有的事务都是阻塞地执行,上一个事务执行完了才能执行下一个事务,这个隔离级别下的数据库的并发能力最差。
2.3事务管理器的beanName
我们在使用@Transactional注解修饰方法的时候可以指定当前方法执行的时候使用的事务管理器。事务管理器也就是TransactionMannager,事务管理器是spring处理事务的核心,通过事务管理器可以进行事务的提交、回滚、挂起等等重要的操作,而我们在使用的时候可以指定我们自定义的事务管理器,在事务管理器创建的时候指定我们要操作的数据源,这个很重要,如果我们没有指定事务管理器的话Spring会使用默认的事务管理器的默认数据源。
2.4 事务超时时间
time参数是设置事务执行的超时时间的,单位是秒,默认的超时时间是-1表示不对超时时间做显示,当然我们在自定义超时时间的时候不能设置地小于-1,否则会抛出异常。
2.5 事务只读
readOnly参数可以设置参数只读,如果这个参数被设置成true的话,如果在事务中进行了写操作那么就会抛出异常。
3.事务切面的执行
当方法被@Transactional修饰了之后会生成一个代理对象。当我们的代码执行到被修饰的方法的时候就会执行我们代理对象的执行方法,这里我们以JDK动态代理为例,和我们之间介绍Spring AOP的内容一样,JDK动态代理对象执行切点方法的时候会去执行JdkDynamicAopProxy类的invoke方法,而在这个方法里面会去执行当前代理对象需要执行的拦截器链。在事务代理对象的执行中我们可以看到这里的拦截器是TransactionIntercptor。
通过代理对象的ReflectiveMethodInvocation执行器的process方法将执行流程传递到我们的事务的拦截器处理,我们打开TransactIonIntercptor的invoke方法,可以看到真正对事务进行处理的方法是该方法里面的proceedWithWithInvocation方法,这个方法是处理事务并触发切点方法的实际方法。
3.1 事务拦截器执行的流程总结
- 获取@Transactional配置的元数据
- 获取TransactionMannager事务管理器
- 将事务管理器转换成PlatformTransactionManager类型
- 获取事务的切点方法
- 创建事务
- 执行切点方法
- 清除事务的ThreadLocal信息
- 提交事务
3.2 获取@Transactional配置的元数据
事务的元数据的配置是通过TransactionAttributeSource类的getTransactionAttribute方法来获取。
通过断点我们进入到getTransactionAttribute方法里面,在这里获取了@Transaction配置的元数据信息。可以看到这里的类型是RuleBasedTransactionAttribute,这个类一些事务配置的基础类,可以通过断点的信息看到这里面的属性就是我们注解里需要配置的属性,包括指定的TransactionMannager的beanName,包括事物隔离界别,包括事务的传播行为,包括事务的超时时间,包括事务是否是只读属性等等。通过这些属性的配置就执行我们后面的事务处理的逻辑。
3.3 获取TransactionMannager事务管理器
TransactionMannager(事务管理器)的获取逻辑很简单,显示判断我们是否在元数据中配置了qualifier属性,这个属性就指定了我们事务在执行的时候使用的事务管理器,事务管理器里面是封装了数据源的,所以不同的事务管理器使用的可能是不同的数据源。在我们获取到了事务管理器的beanName之后就通过beanFactory来获取事务管理器的bean,这里面的逻辑很简单就不在这里赘述了。如果我们没有在元数据中配置qualifier属性,那么spring将会从缓存中获取默认的事务管理器。
3.4 将事务管理器转换成PlatformTransactionManager类型
由于我们上一步获取到的事务管理器只是一个空的定义,那么我们要想通过事务管理器来实现事务的功能那么就需要将我们获取到的事务管理器对象转换成其子类的实现。
在这里调用的是asPlatformTransactionManager方法,从方法名中我们就可以知道这里是将事务管理器转换成了PlatformTransactionManager类型。我们看一下PlatformTransactionManager中定义了什么方法来支持事务的具体实现,我们首先看一下PlatformTransactionManager这个接口上面的注释:这个了是Spring事务实现的核心接口。我们可以看到这个接口中定义了三个方法:getTransaction获取事务管理器,commit提交事务,rollback回滚事务,由此可见这个接口提供了事务的核心功能。
3.5 获取事务的切点方法
接着是获取我们要执行的切点方法的全限定名。
3.6 创建事务
在完成了以上的步骤之后就需要来创建事务了。事务的创建部分调用的方法是createTransactionIfNecessary,这个方法的返回值类型是TransactionInfo,而这个类是我们当前执行事务的执行类TransactionAspectSupport类的一个内部类,这个内部类将之前我们获取到的transactionManager、transactionAttribute、joinpointIdentification、transactionStatus等等属性封装。
在这里我介绍一下transactionStatus这个接口,从接口的注释上面我们可以知道这个注解标志了事务的状态,里面定义了两个方法,一个是hashSavepoint判断当前事务是否包含保存点,还有一个方法是flush刷新当前会话的内容到数据库。
我们继续看createTransactionIfNecessary方法,在这个方法里面首先是获取一个当前事务属性的一个封装类DelegatingTransactionAttribute,这个类中提供了属性的get方法,而且这里默认设置当前的事务名为切点方法的全限定名。
我们重点来看一下getTransaction方法,这个方法里就根据我们配置的事务的传播行为进行了具体的分类处理。我们来看一下具体的执行逻辑:
- 获取事务的元数据
- 获取当前的数据库事务
- 如果当前存在事务就根据已存在的事务进一步对当前要执行的事务进行处理
- 校验元数据中配置的timeout属性是否符合规范
- 如果当前的传播行为是MANDATORY那么就抛出异常(程序执行到这里必然是当前没有事务的)
- 如果要请求再当前没有事务的时候创建新事务那么就创建新事务
- 如果是不需要事务就能执行的传播行为那么就不通过事务来执行
3.6.1 获取当前的数据库事务
我们先来看一下spring事务是怎么获取当前事务的,通过doGetTransaction方法我们可以知道这里是获取了当前会话的数据库连接,再将这个连接封装成ConnectionHolder类型,再将这个连接持有者封装成DataSourceTransactionObject类型返回。
我们简单地看一下ConnectionHolder类,在这个类里面我们看到了两个属性,一个是当前会话的连接一个是当前是否有活跃的事务,由此可知***当前是否存在事务也就是当前的数据库会话下有没有正在执行的事务,而每个线程都会持有一个不同的数据库会话(数据库连接池),所有当前是否有事务存在也就是说当前的线程是否一直在执行别的事务***。
3.6.2 如果当前存在事务就根据已存在的事务进一步对当前要执行的事务进行处理
如果当前就存在事务的话就会调用handleExistionTransaction方法来根据当前存在的事务来对要执行的事务进行处理。这里要明白一个重要的点,当前存在的事务的信息都是被存放在当前线程的ThreadLocalMap里面的。我们来总结一下总体的流程:
- 判断要执行的事务的传播行为是都是Never,回顾一下我们之前说的这种传播行为表示不支持事务,所以这里会直接抛出异常。
- 如果事务传播行为是Not_Supported的话表示不支持事务,那么这时就会通过suspend方法挂起存在的事务,之后不通过事务的方式来执行。
- 如果事务传播行为是Requires_New的话表示需要创建一个新的事务,那么在这里就会挂起当前事务并且开始一个新的事务。
- 如果当前传播行为是Nested的话表示当前事务是一个嵌套式的事务,如果支持保存点的话就设置保存点并使用存在的事务,否则就创建一个新的事务。
- 代码执行到这里就只剩Support和Required了,这些表示使用存在的事务来处理,那么在使用存在的事务之前需要先判断一下当前事务和已存在的事务的隔离级别和只读属性是否一样,如果不一样的话就不能使用存在的事务而是直接抛出异常。
3.6.2.1 挂起当前存在的事务
如果事务的传播行为是REQUIRES_NEW、NOT_SUPPORTED、NESTED的话,如果当前存在事务就会通过suspend方法来挂起存在的事务。代码的主要逻辑如下:
3.6.2.1 开启事务
在挂起了存在的事务之后就需要开启新的事务,在方法startTransaction里面可以看到创建一个新的事务的逻辑。
我们先看一下doBegin方法,这个方法是实际创建事务的方法,在这个方法里面设置了一系列我们之前配置的事务的属性信息到数据库连接,并且开启了数据库事务。
在完成了事务的设置之后需要将我们设置的信息绑定到我们当前的线程上,调用的方法是prepareSynchronization。
3.7 执行事务的切点方法
在完成了事务的创建步骤之后就是执行我们实际的切点方法里面的数据库操作的步骤了,这个步骤的执行是通过执行事务拦截器的proceedWithInvocation方法将流程传递到我们的切点方法中去执行。关于切点方法的传递执行我之前在AOP的文章里面说过了这里就不赘述了。
3.8 异常回滚
我们在执行切点方法的业务逻辑的时候,执行流程可能会失败,这时候就会抛出异常。在这部分代码里面,切点方法的执行是被try-catch-finally包裹起来的,当我们的切点方法抛出异常的时候就去执行catch代码块中的completeTransactionAfterThrowing方法。在这个方法中会根据我们的配置去完成发生异常之后的回滚或者提交操作。
我们点开completeTransactionAfterThrowing方法,可以看到这个方法的逻辑十分简单,只是对是否需要进行回滚做了一个判断,如果需要回滚的话就委托数据库进行回滚否则就提交。
那么什么情况是需要回滚的什么情况下又是不需要回滚的呢?我们看一下方法中的判断条件,可以看到其中有一个rollbackOn方法,这个方法就是判断我们当前的异常类型下的数据库事务是否需要被回滚。我们点开这个方法,里面的逻辑很简单,但是同时也是十分重要,可以看到***只有是RuntimeException和Error类型的异常,spring事务才会对事务进行回滚,当然这是默认的配置,我们可以通过注解配置元数据设置指定的需要回滚的类型。***
配置指定类型异常的回滚用到的元数据字段对应的是rollbackFor,比如我们可以指定切点方法抛出了NullPointerException类型的异常需要被回滚那么具体的配置方法如下:
好了那么接下来我们来看一下回滚操作的逻辑,这部分的逻辑是封装在processRollback方法里面的,总结一下总体的步骤:
-
触发事务同步器(TransactionSynchronization)的beforeCompletion方法,这个步骤主要是完成部分资源的回收和清理工作。 -
判断事务是否有保存点,如果有的话就回滚到保存点。 -
***如果事务是一个新创建的事务的话(这个表示当前的事务不是基于一个已经创建的事务来进行的,比如说一个事务方法调用了另一个传播特性是Request_New事务方法,这样第二个方法就是一个新的事务),回滚当前的事务(只是回滚当前的事务而不会回滚调用者的事务)***。 -
如果当前事务不是一个新的事务,比如第二个事务的传播行为是Support那么这里并不会进行回滚操作而是设置一个rollbackOnly的标记,在当前事务的调用方法执行完成之后进行提交时就会检查一个这个标记是不是true,如果是的话就需要回滚方法一和方法二的全部事务。 -
触发事务同步器的afterCompletion方法,这个步骤主要也是来完成资源回收的。 -
触发回滚完成之后的清理方法cleanupAfterCompletion。
3.9 事务提交
在执行完上面的所有之后所有的数据库操作都完成了,这时需要执行事务的提交,在这里事务的提交操作执行的是commitTransactionAfterReturning方法,我们点开这个方法可以看到具体的提交操作是交给TransactionMannager的commit方法来操作的。
我们点开commit方法看看里面的执行逻辑,首先在事务提交之前还是先检查一下当前的事务是否被设置了rollbackOnly标记,如果被设置了的话表示我们的代码在执行业务逻辑的时候发生了错误,那么就不进行提交而是进行回滚,具体的回滚逻辑我们上面已经说了。
我们继续点开processCommit方法,总结来说这里面的总体逻辑和我们之前说的回滚差不多,也是在事务提交之前和之后先调用一些触发器,但是这里有一个比较重要的点:只有我们当前的事务是一个新事务的时候我们才会对其进行提交,也就是说比如方法1新建了一个事务调用了传播类型为Support类型的方法2,那么在方法2执行完成之后并不会提交事务因为对于方法2来说他所持有的事务并不是一个新的事务,只有等待方法1执行完毕之后才能通过方法1的执行流程来一起提交方法2和方法1的数据。
而且这里也要注意我们***在进行事务的提交操作的时候也是有可能抛出异常的,那么这个时候也会根据异常的类型来进行事务的回滚***,具体的回滚流程我们上面已经讲解了。
|