1. 前言
Spring中有两种实现事务的方式,声明式事务和编程式事务,在SpringBoot的注解开发模式下,只需要对某个Bean的方法标注@Transactional注解便开启了声明式事务,而编程式事务则是手动编码new出事务需要的对象,手动提交事务、手动回滚事务。也就是说声明式事务其实是编程式事务的一个封装,Spring将编程式事务的编码封装起来,我们只需要通过注解就可以实现事务功能。
同时,声明式事务本质上是AOP切面编程,AOP切面编程又是通过代理对象去拦截执行的,因此阅读本文以前最好要理解Spring中的AOP切面编程原理以及动态代理原理,可以参考SpringBoot源码学习之AOP切面编程原理、SpringBoot源码学习之CGLIB动态代理原理。
无论是声明式事务还是编程式事务,其本质上只是对JDBC编程中的数据库连接的获取、事务开启、事务提交、异常回滚等操作的封装。而在Spring的事务编程中,数据库连接是通过数据源获取的,因此还需要一个数据源对象。
2. 准备工作
在SpringBoot中,我们需要引入spring-boot-autoconfigure依赖以便引入Spring事务的配置类,不过这里我们只需要引入更全面的spring-boot-starter依赖即可,它包含了spring-boot-autoconfigure依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
在下图可以看到,spring-boot-autoconfigure的JAR包下的META-INF/spring.factories文件下的存在org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration的配置类路径,此配置类正是SpringBoot注解开发模式下帮我们引入的Spring事务需要的相关配置类。
当SpringBoot程序运行后,自动装配功能会将TransactionAutoConfiguration这个Bean以及其里面的衍生Bean都注入容器中,关于自动装配的讲解可以参考SpringBoot自动装配原理讲解。
3. 源码分析
3.1 EnableTransactionManagement
在TransactionAutoConfiguration配置类中,通过@EnableTransactionManagement注解向Spring容器中注入声明式事务编程所需要的一切资源。
通过@EnableTransactionManagement注解中,mode属性的值是PROXY,因此最终会向容器注入一个ProxyTransactionManagementConfiguration的Bean,它将会向Spring容器注入声明式事务编程即切面编程所需要的Advisor对象、拦截器对象等。
在进行源码解读之前,回顾pring中AOP切面编程的基本原理,以非接口类为例子,Spring会扫描所有切面类并找到一切“通知”方法并将它们封装为Advisor对象。一般的Bean在创建过程中完成实例化、初始化后,会被AnnotationAwareAspectJAutoProxyCreator的初始化后置处理,判断该Bean的所有public方法是否和找到的Advisor对象匹配,若匹配则说明需要将该Bean增强为CGLIB代理对象。
接着将找到匹配的Advisor对象打包构造成一个Callback回调函数赋值给这个代理对象,当代理对象访问目标方法时,会进入到这个回调函数找到匹配的Advisor对象将它们进一步封装为拦截器对象,以一定的顺序、链式调用这些拦截器,最后再访问目标方法。
因此,在SpringBoot中的声明式事务编程中,必须资源包括一个Advisor对象、一个拦截器对象。
3.1 Advisor对象
在此篇SpringBoot源码学习之AOP切面编程原理文章中讲述过,AOP编程中的五种通知需要先创建为Advisor对象,在Spring声明式事务编程中,同样不例外,也需要创建一个事务的Advisor对象。
从下图可以看到,在ProxyTransactionManagementConfiguration中,向容器注入了BeanFactoryTransactionAttributeSourceAdvisor这个Advisor对象,它有两个对象属性,TransactionAttributeSource对象和Advice对象。
3.1.1.1 TransactionAttributeSource对象
TransactionAttributeSource是一个接口,它有两个抽象方法:isCandidateClass和getTransactionAttribute,分别表示传入一个Class对象判断是否是声明式事务编程的候选对象以及提取方法上的@Transactional注解的属性值并封装为TransactionAttribute对象返回。在这里Spring引入了实际类型是AnnotationTransactionAttributeSource的TransactionAttributeSource对象。
public class AnnotationTransactionAttributeSource extends AbstractFallbackTransactionAttributeSource
implements Serializable {
// 判断当前目标bean的class对象是否是声明式事务编程的候选对象,判断方法就是
// 找到其所有Method对象,有没有public的、标注了@Transactional注解的方法
@Override
public boolean isCandidateClass(Class<?> targetClass) {
for (TransactionAnnotationParser parser : this.annotationParsers) {
if (parser.isCandidateClass(targetClass)) {
return true;
}
}
return false;
}
@Override
@Nullable
public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
if (method.getDeclaringClass() == Object.class) {
return null;
}
// 从目标方法上提取@Transactional注解的属性值封装到TransactionAttribute对象
TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass);
return txAttr;
}
}
TransactionAttributeSource对象将在TransactionInterceptor拦截器的拦截方法中会使用到,因为需要用到@Transactional的属性值去创建对应的事务对象模型。
3.1.1.2 TransactionInterceptor对象
TransactionInterceptor起到什么作用呢?其实它正是事务编程的入口,在AOP编程中,Advisor对象只是封装的通知方法,实际执行载体是对应的Advice对象,这些Advice对象均实现了MethodInterceptor接口,在它们重写的invoke(MethodInvocation invocation)方法才是切面编程执行逻辑的地方。
回到ProxyTransactionManagementConfiguration,可以看到TransactionInterceptor也需要一个TransactionAttributeSource对象,因为当事务编程开始工作的时候,需要知道当前目标方法需要的事务资源对象应该怎么创建,这依赖于@Transactional注解属性值,因此会用到TransactionAttributeSource#getTransactionAttribute(Method method, Class<?> targetClass)去获取。
至此,我们已经大概知道了事务编程的入口以及未来必需的对象,但是我们仍需要了解更多、其他的必备知识,这对于完成事务编程的串联很重要。
3.2 TransactionManager对象
在前言就介绍过,事务编程本质上就是JDBC中对数据库连接的封装,那么数据库连接从何而来?在JDBC编程中,数据库连接是通过数据源获取的,这里同样不例外,因此事务编程需要一个数据源对象。
除此之外,还需要一个管理数据源对象的管理对象,它就是TransactionManager对象,因为在TransactionInterceptor拦截器的拦截逻辑中,对于数据源的获取是通过TransactionManager对象的。
从上图可以看出,的确在事务编程中从容器中尝试获取数据源管理对象。并且可以看到,我们甚至可以手动指定当前执行事务的方法可以使用什么数据源管理对象,也就是说多数据源的事务管理是通过@Transactional注解的transactionManager属性指定的。
如果我们并没有手动创建一个TransactionManager的Bean,则SpringBoot的自动装配功能也会帮我们创建一个Bean的名称是transactionManager的DataSourceTransactionManager数据源管理对象,可以看到它持有一个数据源对象属性。
3.2 判断当前Bean是否需要事务编程
声明式事务编程本质上就是一个AOP切面编程,因此需要做一件事情,就是找到哪些Bean对象需要事务编程。对于AOP切面编程还不了解的, 可以参考SpringBoot源码学习之AOP切面编程原理、SpringBoot源码学习之CGLIB动态代理原理。
判断当前Bean是否需要事务编程,这还需要知道一些Spring中Bean生命周期的知识,Bean的声明定义——Bean的实例化(所有属性均为null)——Bean的初始化(填充属性)——Bean的销毁。而对目标Bean进行代理增强为代理对象则是在Bean的初始化的完成以后。
开启切面编程后,Spring容器中有这么一个AnnotationAwareAspectJAutoProxyCreator的Bean,在它的postProcessAfterInitialization(Object bean, String beanName)正是会将初始化完成后的Bean对象做判断,判断它的方法中是否与已经找到的所有Advisor通知对象匹配上,如果是则说明需要被增强为代理对象。
匹配逻辑是从Advsor对象获取一个MethodMatcher对象,调用其 matches(Method method, Class<?> targetClass)去判断是否应该将该Advisor对象应用于当前方法,若判断为true则说明需要应用即需要被增强为代理。
在前面源码分析小节已经详细介绍过,Spring事务编程的Advisor对象是BeanFactoryTransactionAttributeSourceAdvisor,它的MethodMatcher对象是TransactionAttributeSourcePointcut。
TransactionAttributeSourcePointcut重写的match方法如上图所示,主要逻辑就是判断当前的Method对象是否有@Transactional注解标注并将该注解属性封装为一个TransactionAttribute对象,若不为空则表示当前Bean开启了声明式编程,需要被增强为代理对象。
3.3 事务编程执行逻辑
在Spring中,非接口默认会被增强为CGLIB对象,如果对其原理仍不熟悉的可以参考SpringBoot源码学习之CGLIB动态代理原理,这里不再赘述。
从前面源码分析小节,我们已经知道,事务编程的入口处是TransactionInterceptor#invoke(...)方法,具体执行又是通过父类TransactionAspectSupport#invokeWithinTransaction(...)完成的,那么这里我们便直入主题。
从上图可以看到,前面分析的三个对象都在这里派上用场了,先是TransactionAttributeSource对象,利用它将当前目标方法的@Transactional注解的属性值封装为TransactionAttribute对象,它拥有事务传播行为、事务隔离级别、超时时间、需要回滚的异常类、是否只读事务等属性。然后是TransactionManager对象,它拥有一个数据源对象——提供数据库连接的获取。
我们仍然需要进一步知道这三个对象究竟具体发挥什么作用,因为JAVA是面向对象编程,事务操作的任何资源都被封装为这些对象,它们之间是如何关联、协同工作是必须掌握的。尤其是TransactionAttribute和TransactionManager。
3.3.1 TransactionAttribute的作用
从下图可以看出,TransactionAttribute有rollbackRules、qualifier、propagationBehavior、timeout、readOnly等属性。
rollbackRules则是回顾规则,我们可以通过在@Transactional注解声明rollbackFor和notRollbackFor,它们表示哪些异常需要回顾、哪些异常不需要回顾。
qualifier则通过@Transactional注解声明transactionManager属性,它表示当前目标方法的事务执行使用哪个TransactionManager对象。
propagationBehavior可通过@Transactional注解声明propagation属性,它表示事务的传播行为,为什么叫传播行为呢?因为可能存在多事务,因此需要传播行为去管理。
timeout可通过@Transactional注解声明timeout属性,它表示数据库连接的超时时间。
readOnly可通过@Transactional注解声明readOnly属性,它表示事务是否只读,但是经过实践即便设置了为true,发生异常仍然会回顾事务。
至于其他属性,有兴趣的可自行研究。
3.3.2 TransactionManager的作用
Spring帮我们创建的DataSourceTransactionManager对象,在TransactionInterceptor中被强转为了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;
}
可以看到,PlatformTransactionManager有三个方法,分别是getTransaction、commit、rollback,后面两个方法大家也可以猜到是做什么的,没错就是做事务的提交和回顾操作。也就是说,将DataSourceTransactionManager强制转换的原因很可能只是代码架构上的一个优化,通过一个定义了特定方法的中间对象去做一些事情,简单来说就是将复杂操作封装到简单的方法入口中,让读者看起来更清晰明了?
这三个方法中均使用了一个TransactionStatus对象,再结合commit和rollback方法,也就是说数据库连接等资源应该是封装到了这个TransactionStatus对象。那么它是怎么创建的呢?
3.3.3 TransactionStatus的作用
在PlatformTransactionManager#getTransaction(TransactionDefinition definition)方法中正是创建TransactionStatus对象的地方,从下图可以看到,这里面做了几步操作,第一步就是通过doGetTransaction方法创建一个transaction对象;判断当前线程是否已经存在事务,是则走handleExistingTransaction方法处理;判断超时时间是否合法,不合法则抛异常;按照事务传播行为的不同走不同的分支处理或创建TransactionStatus对象。
3.3.3.1 DataSourceTransactionObject对象
在doGetTransaction()方法中,会创建一个DataSourceTransactionObject对象,并且将一个通过TransactionSynchronizationManager.getResource(obtainDataSource())获取的对象赋值给它的ConnectionHolder对象属性。其实从字面意思能猜出这个ConnectionHolder起着什么作用,没错它就是负责保存一个数据库连接对象。
而在TransactionSynchronizationManager.getResource(obtainDataSource())中可以看到,根据当前DataSourceTransactionManager所持有的数据源对象,从其ThreadLocal变量获取一个绑定在当前线程的ConnectionHolder对象,不过当线程第一次进入此处时获取的值是null的,因为至此我们还没看见ConnectonHolder对象的初始化。
这里要注意的一点是TransactionSynchronizationManager的所有保存事务资源的变量对象都是类型为ThreadLocal的,也就是说事务跟当前线程有关,异步线程里将获取不到主线程创建并绑定的事务资源!
虽然我们还清楚ConnectonHolder对象的初始化在哪里进行的,但是我们知道了这个doGetTransaction()方法创建的对象最终肯定会拥有一个数据库连接对象,记住这一点将非常重要。
3.3.3.2 TransactionStatus对象
从上面可以看到,不同事务传播行为最终执行的分支不一样,假设当前线程此前没有事务,则如果传播行为是MANDATORY的话会直接抛异常;如果是REQUIRED、REQUIRES_NEW、NESTED的话,会将DataSourceTransactionObject对象作为入参调用startTransaction(...)去创建;如果是剩下的SUPPORT、NOT_SUPPORT、NEVER传播行为的话,则是调用prepareTransactionStatus(...)创建,不过可以看到传递的transaction变量是null值,这里的null值是代表什么呢?
以MySql为例,如果不开启事务,则每个相同方法里sql执行都是去新创建一个SqlSession对象完成的,但是数据库连接对象却是每执行一条sql而获取一个并且不会保存下来。前面也分析过,transaction对象最终会保存着数据库连接对象,那么在SUPPORT、NOT_SUPPORT、NEVER传播行为下,传递的transaction是null,也就是这些事务下使用的数据库连接对象并不是共享的,即可以推断这三个的传播行为的方法其实是以一种“非事务”的方式执行的。
不过在这里我们不考虑当前线程有多个事务的情况,这里只以最常见的单事务、传播行为是默认的REQUIRED去进行源码解读。
在startTransaction(...)方法中可以看到,创建了一个实际类型为DefaultTransactionStatus的对象,它包含了TransactionAttribute对象(实现了接口TransactionDefinition)、DataSourceTransactionObject对象、两个布尔属性newTransaction是写死的为true值,而newSynchronization也为true,对于前面两个对象我们都很熟悉它们的作用了,但是这两个布尔属性究竟有什么作用呢?源码分析到目前为止还不清楚,我们需要在这里留下一个疑问。
接着,让我们继续往下分析doBegin(transaction, definition)的方法,在这里正是做数据库连接初始化的工作。在下面伪代码可以看到,由于前面创建的DataSourceTransactionObject对象中的ConnectionHolder对象是null值,因此会在这里从绑定的数据源对象获取一个新的数据库连接对象并绑定到ConnectionHolder中,并标记这个DataSourceTransactionObject对象的newConnectionHolder属性为true,表示这是一个新的连接持有者对象。接着为ConnectionHolder的transactionActive属性设置为true,表示拥有此数据库连接持有者的事务正在激活。接着又将数据库连接对象的自动提交设置为false,最后通过TransactionSynchronizationManager将它绑定到ThreadLocal变量中去。
public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
implements ResourceTransactionManager, InitializingBean {
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
// 从绑定的数据源中获取一个新的数据库连接
Connection newCon = obtainDataSource().getConnection();
// 将数据库连接绑定到DataSourceTransactionObject中
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
if (con.getAutoCommit()) {
// 等事务提交或者回滚后会将数据库连接重新设置为自动提交
txObject.setMustRestoreAutoCommit(true);
con.setAutoCommit(false);
}
// 将数据库连接持有对象的transactionActive设置为true,表示当前持有
// 数据库连接的对象的事务正在激活
txObject.getConnectionHolder().setTransactionActive(true);
// 将数据库连接对象绑定到当前ThreadLocal变量去
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(),
txObject.getConnectionHolder());
}
}
}
}
当doBegin(...)方法结束后,一个TransactionStatus对象的创建已经完成了,此时它已经拥有了事务操作的底层依赖——数据库连接对象、以及两个仍然不清楚有何作用的布尔变量newTransaction、newSynchronization等资源。
3.3.4 TransactionInfo对象
当TransactionAttribute、TransactionManager、TransactionStatus三个对象都创建完毕后,会用一个TransactionInfo继续封装它们,不过这仅仅是为了解决多个对象在方法中的传递问题,毕竟传递一个对象总比传递多个对象方便。
3.3.5 开始事务编程
在3.3.1-3.3.4的小节中介绍了TransactionAttribute、TransactionManager、TransactionStatus三种对象的创建或作用,它们都是事务编程所需要的必要资源。当事务编程所需要的资源一切都准备完毕了,就要开始事务编程了。本小节将以如下伪代码进行事务编程的源码讲解,方法中做了更新操作,并且制造一个除零异常。
@Transactional(propagation = Propagation.REQUIRED)//默认传播行为Propagation.REQUIRED
public void testA() {
String value = testDao.doQuery(); //高压锅
testDao.doUpdate();// 将高压锅更改为电饭锅
int l = 5/0; // 模拟报错回滚更新操作
}
事务编程是在拦截器TransactionInterceptor的invoke(...)进行的,不过最终会进入到它的父类TransactionAspectSupport #invokeWithinTransaction(...)处理的。
从下面伪代码可以看出,其实声明式事务编程本质上是一个环绕通知,在目标方法调用前后做了特定的操作,在这里体现为前置操作是准备事务编程的资源对象、中间操作在try-catch代码块中调用目标方法(目标方法中可能会有嵌套的调用其他方法,但它总是会以链式调用方式执行)、后置操作则是等目标方法执行完毕后尝试提交事务或者(若catch到异常则)回滚事务(然后往上抛出异常)。
public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
TransactionAttributeSource tas = getTransactionAttributeSource();
// 封装@Transactional的属性对象
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final TransactionManager tm = determineTransactionManager(txAttr);
// 事务管理器,负责事务提交、回滚等操作
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
// 目标方法连接点标识,全限定类名+方法名
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
// 封装了事务操作的一切资源的对象
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;
}
}
3.3.5.1 事务的回滚
事务的回滚具体是怎么操作的呢?在testA()方法中做了数据库的查询&更新操作并在最后面模拟除零等异常行,因此在运行时会抛出一个除零异常。
TransactionAspectSupport #invokeWithinTransaction(...)在捕获到异常以后,从下图可以看到,它会先把异常拿去校验是否属于应该回滚的异常,在@Transactional注解有两个关于异常的熟悉rollbackFor和noRollbackFor,分别表示等需要回滚的异常和不需要回滚的异常,从字面意思就能猜到大概,也就是说我们可以人为控制哪些异常需要回滚或者不回滚而进行事务提交,这里不再赘述。
最终的回滚处理是通过AbstractPlatformTransactionManager#processRollback(...)处理的,从下图我们可以看到,只有当TransactionStatus对象的newTransaction布尔变量为true时才会进入doRollback(status)方法真正回滚,至此终于可以解读上面的疑问了:newTransaction为true时才能执行事务回滚!
接着深入探究doRollback(status)方法,它是DataSourceTransactionManager重写的方法,逻辑非常简单就是从TransactionStatus对象获取当前绑定的数据库连接对象调用其原生的rollback()方法,也就是说所有在此数据库连接对象执行过的更新操作都将会被回滚!
3.3.5.2 事务提交
@Transactional(propagation = Propagation.REQUIRED)//默认传播行为Propagation.REQUIRED
public void testA() {
String value = testDao.doQuery(); //高压锅
testDao.doUpdate();// 将高压锅更改为电饭锅
// int l = 5/0; // 模拟报错回滚更新操作
}
当testA()没有制造异常后,即目标方法没有发生异常时,便会进行事务提交的操作,如下图所示,它仍然是通过TransactionManager#commit(status)处理的。
在AbstractPlatformTransactionManager#processCommit(DefaultTransactionStatus status)便是真正处理事务提交的事项,不过这里和事务回滚有一些异同,相同之处还是只有当TransactionStatus的newTransaction布尔变量为true时才会真正做数据库连接的提交操作。不同之处就是,当事务提交了后,就要将当前绑定的事务资源做清除工作。
AbstractPlatformTransactionManager#cleanupAfterCompletion(DefaultTransactionStatus status)是负责事务资源的清理工作的,可以看到这里可能会干三件事情:
①清除当前同步块绑定在当前线程的资源
②清除当前线程绑定的ConnectionHolder对象、即清除绑定的数据库连接对象
③如果当前事务有挂起的资源则唤醒恢复它们,这在多事务中、并且子事务的传播行为是REQUIRED_NEW中很常见。
本小节将会对前两件事情做深入讲解,可以看到清除当前同步块绑定在当前线程的资源也是有前置条件的,那就是TransactionStatus对象的newSynchronization布尔变量是true时,在testA()方法的例子中创建的TransactionStatus对象的时候就此变量就赋值为true了。从下图可以看出,其实只是对这些ThreadLocal变量的清除,不过或许是testA()的例子是最简单的、单事务、没有调用嵌套事务方法,因此体现不到这些变量有何作用,有兴趣的话可以后面深入研究。
第二件事,当TransactionStatus对象的newTransaction布尔变量为true时会进入到DataSourceTransactionManager#doCleanupAfterCompletion(Object transaction)方法中,这里做的都是释放资源的工作,首先可以看到只有newConnectionHolder布尔变量是true时才会通过TransactionSynchronizationManager.unbindResource(obtainDataSource())解除当前线程绑定的ConnectionHolder对象,接着为数据库连接对象重新设置自动提交、隔离级别等,最后也是当newConnectionHolder=true时将数据库连接做释放或者关闭操作。
那么这个newConnectionHolder什么时候才是true呢?其实这个也跟事务的多寡和其传播行为有直接关系,如果只有一个事务,那么newConnectionHolder必定是true,因为它是初始化的时候创建的,但是有多个事务的话,则不一定,因为有些多事务会继承原来事务的资源,那么此时就不能设置为true了,否则多事务做提交操作的时候会提前清除数据库连接对象等资源从而影响原来事务的提交。
3.3.5.3 不同的事务传播行为
事务传播类型 | 当前线程没有事务 | 当前线程有事务 |
REQUIRED | 新建一个事务,以事务方法执行 | 新建事务但是共享旧事务的数据库连接等资源,若子事务回滚会影响父事务 |
REQUIRES_NEW | 新建一个事务,以事务方法执行 | 直接新建一个事务,将旧事务的资源挂起来,自己重新获取数据库连接等资源 |
NESTED | 新建一个事务,以事务方法执行 | 创建新事务同时创建一个保存点并且共享旧事务的资源,如果子事务发生异常可回滚到保存点,不影响父事务。但是父事务没有保存点的话,回滚会将子事务也回滚了。 |
SUPPORTS | 新建一个事务,实际以非事务方法执行,即无提交&回滚 | 新建事务但是共享旧事务的数据库连接等资源,若子事务回滚会影响父事务 |
NOT_SUPPORTED | 新建一个事务,实际以非事务方法执行,即无提交&回滚 | 创建一个事务并且挂起旧事务的资源,但实际以非事务执行方法,执行完成后再唤醒资源 |
NEVER | 新建一个事务,实际以非事务方法执行,即无提交&回滚 | 报错、抛出当前存在事务的情况下不支持此传播行为的异常 |
MANDATORY | 报错、抛出不支持此传播行为的异常 | 新建事务但是共享旧事务的数据库连接等资源,若子事务回滚会影响父事务 |
以上就是七种事务传播行为的详细情况,将在第4章节进一步验证。
4. 多事务下的案例
本章节将以1×7的方式去讲解多事务,即第一个事务方法里调用另外一个事务方法。第一个方法如下,它的事务传播行为将设置为REQUIRED,并且在调用其他事务方法的时候,用了try-catch机制。第二个事务方法则会制造报错,抛出异常,验证多事务的回滚情况。
@Component
public class TransactionBean {
@Transactional(propagation = Propagation.REQUIRED)
public void testTransaction(){
System.out.printf("未更新前的值为"+testDao.doQuery());
testDao.doUpdate("砂锅煲1");
try{
// 注意看这里为什么要用try-catch机制?
transactionBeanPlus.testTransactionPlus();
}catch (Exception e){
e.printStackTrace();
}
}
}
为什么要使用try-catch机制呢?还记得拦截器TransactionInterceptor是怎么样调用目标方法的吗?如下图所示,当调用目标方法时,其实是一个链式调用,如果目标方法里面调用了其他的事务方法并发生异常的话,会先尝试做事务回滚等操作,完成以后还会将异常再次抛到上层。
也就是说,如果在传播行为是REQUIRED的父事务中,不try-catch调用子事务方法的话,当子事务方法报错必然会将异常传递到父事务方法中,这样无论如何父事务都会被迫回滚,也就无法验证多事务下的不同情况了。
4.1 REQUIRED
子事务如下伪代码所示,传播行为是REQUIRED,在3.3.5.3 不同的事务传播行为下介绍了,若存在多事务并且子事务是REQUIRED的时候,会和父事务共享已经绑定的资源,即共享一个数据库连接对象。也就是说,由于父子事务都是用同一个数据库连接,即若在子事务发生回滚了,父事务无论有没有catch子事务的异常,父事务都会被迫跟着回滚。
@Component
public class TransactionBeanPlus {
@Transactional(propagation = Propagation.REQUIRED)
public void testTransactionPlus(){
testDao.doUpdatePlus("高压锅1");
int k = 5 / 0;// 制造报错,让当前事务回滚
}
}
未执行方法时,数据如下,testDao.doUpdate("砂锅煲1")和testDao.doUpdatePlus("高压锅1")分别会更新goods_id为1和2的数据。
执行方法后,如下图最新数据所示,子事务中产生了除零异常,则传播行为是REQUIRED的子事务必然产生回滚,并且会牵连同是REQUIRED的父事务,场景验证成功。
4.2 REQUIRES_NEW
在3.3.5.3 不同的事务传播行为下介绍了,不管有没有父事务,REQUIRES_NEW传播行为的都会新创建一个属于自己的事务,如此前存在父事务则会将父事务的资源挂起来(因为当前事务资源是同当前线程绑定的),等子事务完成后再将父事务的资源唤醒。也就是说在下面伪代码中,回滚的只有goods_id=2的数据,goods_id=1的将会更改为砂锅煲1。
@Component
public class TransactionBeanPlus {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void testTransactionPlus(){
testDao.doUpdatePlus("高压锅1");
int k = 5 / 0;// 制造报错,让当前事务回滚
}
}
未执行方法时,数据如下,testDao.doUpdate("砂锅煲1")和testDao.doUpdatePlus("高压锅1")分别会更新goods_id为1和2的数据。
执行方法后,如下图最新数据所示,子事务中产生了除零异常,则传播行为是REQUIRED_NEW的子事务必然产生回滚,但是并没有影响到REQUIRED的父事务,因为它们的数据库连接资源不是共享的,场景验证成功。
4.3 NESTED
在3.3.5.3 不同的事务传播行为下介绍了NESTED的特点,无论此前有没有事务都新建一个事务,但是若有父事务,则仍然使用父事务的数据库连接资源但同时会创建一个保存点,这和REQUIRES_NEW的子事务的完全使用新资源不同。也就是说,虽然这种情况下都是共用一个数据库连接对象,但是NESTED子事务只会回滚到保存点,即不会影响父事务的回滚。但是要注意,若父事务回滚了,子事务也会跟着被回滚。
因此这可以与4.1 REQUIRED的执行结果直接对比,两者都是共用父事务的数据库连接资源,但是前者都会回滚,后者只有子事务回滚。
@Component
public class TransactionBeanPlus {
@Transactional(propagation = Propagation.NESTED)
public void testTransactionPlus(){
testDao.doUpdatePlus("高压锅1");
int k = 5 / 0;// 制造报错,让当前事务回滚
}
}
未执行方法时,重置的数据如下,testDao.doUpdate("砂锅煲1")和testDao.doUpdatePlus("高压锅1")分别会更新goods_id为1和2的数据。
执行结果符合推断,只有子事务回滚了,父事务的数据更新成功。并且从结果上看,NESTED子事务和REQUIRES_NEW子事务的回滚效果如出一辙,一个是只回滚到父事务的那个节点、一个则是用全新的资源不影响父事务。
4.4 NOT_SUPPORTED
在3.3.5.3 不同的事务传播行为下介绍了NOT_SUPPORTED的传播行为,如果此前没有事务,则以非事务方式执行,若此前有事务,则挂起父事务资源,自己也创建一个事务对象,但实际以非事务执行。也就是说当前的子事务即便发生异常了,更新的SQL仍然会被执行!
@Component
public class TransactionBeanPlus {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void testTransactionPlus(){
testDao.doUpdatePlus("高压锅1");
int k = 5 / 0;// 制造报错,让当前事务回滚
}
}
未执行方法时,重置的数据如下,testDao.doUpdate("砂锅煲1")和testDao.doUpdatePlus("高压锅1")分别会更新goods_id为1和2的数据。
执行结果仍然符合预期,当NOT_SUPPORTED的子事务方法发生异常,数据仍然被更新成功,即以“非事务”的方法(非事务则是执行一条SQL就从数据源获取一个新的连接,该连接是自动提交)执行。
4.5 SUPPORTS
在3.3.5.3 不同的事务传播行为下介绍了SUPPORTS的传播行为, 若此前没有事务则实际以非事务执行,若有父事务,则也创建一个新事务,但是也是和父事务共享数据库连接等资源。也就是最终子事务的回滚会带着父事务回滚,无论父事务方法有没有catch子事务方法的异常。
@Component
public class TransactionBeanPlus {
@Transactional(propagation = Propagation.SUPPORTS)
public void testTransactionPlus(){
testDao.doUpdatePlus("高压锅1");
int k = 5 / 0;// 制造报错,让当前事务回滚
}
}
未执行方法时,重置的数据如下,testDao.doUpdate("砂锅煲1")和testDao.doUpdatePlus("高压锅1")分别会更新goods_id为1和2的数据。
执行结果和前面分析的一样,虽然SUPPORTS的子事务是新建事务但是数据库连接资源等还是用的父事务的,因此在子事务发生回滚后也影响了父事务。
4.6 MANDATORY
在3.3.5.3 不同的事务传播行为下介绍了MANDATORY的传播行为,若此前没有事务,则直接抛异常声明不支持事务,但是若此前有事务,则同样新建一个事务但是资源也是和父事务共享,也就是若子事务是MANDATORY,其最终验证结果和4.1 REQUIRED、4.5 SUPPORTS一样,子事务发生异常将牵连父事务一起回滚。
@Component
public class TransactionBeanPlus {
@Transactional(propagation = Propagation.MANDATORY)
public void testTransactionPlus(){
testDao.doUpdatePlus("高压锅1");
int k = 5 / 0;// 制造报错,让当前事务回滚
}
}
未执行方法时,重置的数据如下,testDao.doUpdate("砂锅煲1")和testDao.doUpdatePlus("高压锅1")分别会更新goods_id为1和2的数据。
果然,结果和REQUIRED、SUPPORTS的子事务一样,父事务跟着子事务一起回滚了。
4.7 NEVER
在3.3.5.3 不同的事务传播行为下介绍了NEVER的传播行为,若此前没有事务,则新建一个事务,但实际以非事务执行,若此前有事务,则直接抛异常声明不支持在存在事务的情况允许NEVER的子事务存在。
@Component
public class TransactionBeanPlus {
@Transactional(propagation = Propagation.NEVER)
public void testTransactionPlus(){
testDao.doUpdatePlus("高压锅1");
int k = 5 / 0;// 制造报错,让当前事务回滚
}
}
未执行方法时,重置的数据如下,testDao.doUpdate("砂锅煲1")和testDao.doUpdatePlus("高压锅1")分别会更新goods_id为1和2的数据。
从下图可以看到,父事务的更新SQL执行成功了, 子事务方法没有看到除零异常,而是抛了一个在当前线程已有事务情况下发现了不支持的“never”传播行为的子事务。
4.8 多事务总结
在本章节中,介绍了多事务下一共七种情况的异常回滚详情,从中可以归纳出,对于子事务来说其实一共有5种情况:
①以REQUIRED、SUPPORTS、MANDATORY的子事务为例,使用的数据库连接资源是父事务创建的,因此子事务回滚会影响父事务,父事务回滚会影响子事务。
②以REQUIRES_NEW的子事务为例,使用的数据库连接资源是新建的,父事务的资源被挂起,因为子事务回滚影响不了父事务,父事务回滚也影响不了子事务。
③以NESTED的子事务为例,使用的数据库连接资源是父事务创建的,但是同时拥有一个保存点,子事务回滚不会影响父事务,但是父事务回滚会影响子事务。
④以NOT_SUPPORTED的子事务为例,虽然也创建一个新的事务,但实际以非事务执行。执行的时候会挂起父事务资源,因此不存在子事务回滚一说,父事务回滚也不会影响子事务。
⑤以NEVER的子事务为例,没有进入到子事务方法时就被拦截并抛异常了,因为在多事务情况下不允许传播行为是NEVER的子事务。
5. 全文总结
本文主要介绍了在SpringBoot中,通过@Transactional注解实现声明式事务编程的主要过程,包括相关重要类对象的创建和用途讲解、事务的回滚、事务的提交、不同情况下的案例讲解。
用一句话归纳为就是,Spring事务编程帮我们利用一个数据源,通过一种正确的组织方式去实现数据库连接的获取和销毁或释放,并且根据事务传播行为的不同去合理分配资源或挂起资源,达到正确的事务回滚和提交。