spring之事务处理

Spring之事务管理

一、简单叙述事务

事务:在关系数据库中,一个事务可以是一条SQL语句,一组SQL语句或整个程序。事务执行过程中,必须要在同一个数据库连接中,然后会有开启事务、提交事务或者是回滚事务。伪代码如下所示:

Connection connection = getConnectionFromDataSource();
connnection.setAutoCommit(false);
try{
    executeSql();
    connection.commit();
}catch(Throwable e){
    connection.rollbak();
}finaly{
    connection.close();
}

在Spring框架中考虑到了获取得到数据库连接、开启事务、提交事务或者回滚事务,但是Spring框架开发旨在让开发人员更关注于业务逻辑方法。

在Spring中进行事务管理非常简单,只需要在方法上加上注解@Transactional,Spring就可以自动帮我们进行事务的开启、提交、回滚操作。
甚至很多人心里已经将Spring事务与@Transactional划上了等号,只要有数据库相关操作就直接给方法加上@Transactional注解。

那么如果要是这么想的话,那么对Spring的事务理解就存在问题了。

二、@EnableTransactionManagement注解工作原理

开启Spring事务本质上就是增加了一个Advisor,但我们使用@EnableTransactionManagement注解来开启Spring事务是,该注解代理的功能就是向Spring容器中添加了两个Bean:

  1. AutoProxyRegistrar
  2. ProxyTransactionManagementConfiguration

AutoProxyRegistrar主要的作用是向Spring容器中注册了一个InfrastructureAdvisorAutoProxyCreator的Bean。

而InfrastructureAdvisorAutoProxyCreator继承了AbstractAdvisorAutoProxyCreator,所以这个类的主要作用就是开启自动代理的作用,也就是一个BeanPostProcessor,会在初始化后步骤中去寻找Advisor类型的Bean,并判断当前某个Bean是否有匹配的Advisor,是否需要利用动态代理产生一个代理对象。

ProxyTransactionManagementConfiguration是一个配置类,它又定义了另外三个bean:

  1. BeanFactoryTransactionAttributeSourceAdvisor:一个Advisor
  2. AnnotationTransactionAttributeSource:相当于BeanFactoryTransactionAttributeSourceAdvisor中的Pointcut
  3. TransactionInterceptor:相当于BeanFactoryTransactionAttributeSourceAdvisor中的Advice
Advisor
    PointCut
    	ClassFilter:类过滤器
        MethodMatcher:方法匹配器
    Advice:对连接点进行增强的逻辑

AnnotationTransactionAttributeSource就是用来判断某个类上是否存在@Transactional注解或者判断某个方法上是否存在@Transactional注解的;

TransactionInterceptor就是代理逻辑,当某个类中存在@Transactional注解时,到时就产生一个代理对象作为Bean,代理对象在执行某个方法时,最终就会进入到TransactionInterceptor的invoke()方法;

2.1、联合bean场景

一个Bean在执行Bean的创建生命周期时,会经过InfrastructureAdvisorAutoProxyCreator的初始化后的方法,会判断当前当前Bean对象是否和BeanFactoryTransactionAttributeSourceAdvisor匹配,匹配逻辑为判断该Bean的类上是否存在@Transactional注解或者类中的某个方法上是否存在@Transactional注解,如果存在则表示该Bean需要进行动态代理产生一个代理对象作为Bean对象。

该代理对象在执行某个方法时,会再次判断当前执行的方法是否和BeanFactoryTransactionAttributeSourceAdvisor匹配,如果匹配则执行该Advisor中的TransactionInterceptor的invoke()方法,执行基本流程为:

  1. 利用所配置的PlatformTransactionManager事务管理器新建一个数据库连接;
  2. 修改数据库连接的autocommit为false;
  3. 执行MethodInvocation.proceed()方法,简单理解就是执行业务方法,其中就会执行sql;
  4. 如果没有抛异常,则提交;
  5. 如果抛了异常,则回滚;

2.2、源码分析

在添加@EnableTransactionManagement注解之后为什么就可以使用Spring事务了呢?

@EnableTransactionManagement注解导入了TransactionManagementConfigurationSelector类,而这个类实现了ImportSelector接口,那么就有了ImportSelector接口中的功能。

导入了两个类:AutoProxyRegistrarProxyTransactionManagementConfiguration

AutoProxyRegistrar

这个类从名称上来看就是要注册BeanDefinition的,这个类实现了ImportBeanDefinitionRegistrar接口,那么看一下对应的registerBeanDefinitions方法

点击进去发现住了一个InfrastructureAdvisorAutoProxyCreator类型的BeanDefinition,那么看一下继承体系

发现了InfrastructureAdvisorAutoProxyCreator也是一个AbstractAutoProxyCreator后置后处理,这个跟AOP导入进来的类是一样的。

所以使用@EnableTransactionManagement注解和@EnableAspectJAutoProxy的功能有了相似的功能,都能够进行AOP。

注意

@EnableAspectJAutoProxy注解导入的是AnnotationAwareAspectJAutoProxyCreator类型的,能够来解析得到PointCut和Advisor类型以及类上添加了@AspectJ注解的bean;而@EnableTransactionManagement注解导入的是InfrastructureAdvisorAutoProxyCreator类型的,那么这个类型应该完成能够找到在类上或者是方法上能够找到@Transactional注解的bean。也就是说每个子类各自干各自的事情,父类只不过是提供了一个模板。

二者虽然都导入了AbstractAutoProxyCreator(BeanPostProcessor)进行AOP,但是在找对应的Advisor的时候的实现是不同的。

所以要是想使用事务,只需要添加上@EnableTransactionManagement注解;如果还想使用自定义的切面逻辑,那么还需要添加上@EnableAspectJAutoProxy注解。

ProxyTransactionManagementConfiguration

直接看下这个类,做为一个配置类,定义了Advisor、PointCut和Advice三个bean。

从第一个配置的Bean中可以看到,定义了一个Advisor。那么可以看下它定义的PointCut和Advice。

创建了一个BeanFactoryTransactionAttributeSourceAdvisor对象,然后设置了PointCut和Advice,而对于PointCut和Advice来说,又是从方法参数中获取得到的,对于@Bean方法来说,参数对应的bean是从容器中获取得到的。

TransactionAttributeSourcePointcut

我们直接从BeanFactoryTransactionAttributeSourceAdvisor类中去找PointCut,可以看到返回的是一个对象

private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() {
    @Override
    @Nullable
    protected TransactionAttributeSource getTransactionAttributeSource() {
        return transactionAttributeSource;
    }
};

从这个类中进去,找到类过滤器和方法匹配器。

protected TransactionAttributeSourcePointcut() {
    setClassFilter(new TransactionAttributeSourceClassFilter());
}

那么类过滤器就应该有具体的过滤逻辑。

  • 1、如果当前bean是TransactionalProxy、TransactionManager、PersistenceExceptionTranslator类型的,那么就直接返回false;
  • 2、将AnnotationTransactionAttributeSource对象(注解中配置的属性信息)传入进来作为参数;
  • 3、判断类中的信息是否满足条件(类上和方法上是否有@Transactional注解信息)

那么看一下具体的判断信息

利用TransactionAnnotationParser事务注解解析器来进行解析,那么是哪个解析器呢?

那么看添加的是哪个注解,一般说来是,添加的是javax.transaction.Transactional

为什么是这个呢?因为在org.Springframework.transaction.annotation.ProxyTransactionManagementConfiguration#transactionAttributeSource中的构造函数调用过来的。最终确定使用的注解解析器是SpringTransactionAnnotationParser。

那么看看是如何来进行解析的。

应该就是和猜想一样的,通过参数来判断的。那么继续跟进

之前也看到了:javax.transaction.Transactional是javax包下的。

而对于org.Springframework.core.annotation.AnnotationsScanner#hasPlainJavaAnnotationsOnly(java.lang.Class<?>)来说:

static boolean hasPlainJavaAnnotationsOnly(Class<?> type) {
    return (type.getName().startsWith("java.") || type == Ordered.class);
}

判断类名是否以java.开头或者类型是Ordered类型的,如果不是,进行下一个判断。

所以对于类过滤器来说,并没有判断类上或者是方法上是否添加了Transactional注解。

那么来看下方法匹配器是怎么来进行匹配的。

对于TransactionAttributeSourcePointcut类来说,继承了StaticMethodMatcherPointcut这个类

public final MethodMatcher getMethodMatcher() {
    return this;
}

那么返回的方法匹配器就是TransactionAttributeSourcePointcut类中的匹配方法

public boolean matches(Method method, Class<?> targetClass) {
    TransactionAttributeSource tas = getTransactionAttributeSource();
    return (tas == null || tas.getTransactionAttribute(method, targetClass) != null);
}

bean的方法在执行的时候,这里会拿到方法和被代理的目标类型进来,然后用封装的事务属性信息来进行判断。

因为getTransactionAttributeSource得到的对象是AnnotationTransactionAttributeSource类型的,那么对应的getTransactionAttribute方法AnnotationTransactionAttributeSource类中没有重写,直接调用父类org.Springframework.transaction.interceptor.AbstractFallbackTransactionAttributeSource#getTransactionAttribute中的方法

那么问题就来到如果获取得到@Transactional注解信息的地方

TransactionAttribute txAttr = findTransactionAttribute(specificMethod);

来到org.Springframework.transaction.annotation.AnnotationTransactionAttributeSource#findTransactionAttribute(java.lang.reflect.Method)方法中来

然后看看是如何来进行解析的,进入到org.Springframework.transaction.annotation.SpringTransactionAnnotationParser#parseTransactionAnnotation(java.lang.reflect.AnnotatedElement)方法中来

如果不为空的话,那么进入到org.Springframework.transaction.annotation.SpringTransactionAnnotationParser#parseTransactionAnnotation(org.Springframework.core.annotation.AnnotationAttributes)

RuleBasedTransactionAttribute这个对象将@Transactional注解的全部信息设置到属性中来。

注意:这个对象和rollbackFor属性是有关的。

至此,分析TransactionAttributeSource(PointCut)结束

TransactionInterceptor

对拦截方法的增强,因为TransactionInterceptor实现了MethodInterceptor,那么必然实现了对应的invoke方法。当事务开启之后方法上有@Transactional注解的话或者直接在类上添加了@Transactional注解,Spring会来进行解析生成对应的代理对象。

但是举个例子:

@Component
public class A{
    
    @Transactional
    public void a(){}
    
    public void b(){}
}

在代理对象执行的时候,在执行a方法的时候,会执行到TransactionInterceptor类中的invoke方法中来。而在代理对象执行b方法的时候,因为没有对应的advice,则是直接执行到目标方法,而不会执行到TransactionInterceptor类中的invoke方法中来。

三、自己设计Spring事务实现思想

3.1、正常情况

那么我们先用伪代码来分析一波

1、从Spring事务管理器中获取得到连接connection;
2、利用connection.setAutoCommit(false);
3、target.executeSql();
4、connection.commit/rollback;

而对于我们的业务方法来说,如下:

@Transactional
public void executeSql(){
  jdbcTempate.executeSql();
  mybatisTemplate.executeSql();
}

对于jdbcTempate和mybatisTemplate来说,还需要获取得到connection来执行SQL。那么如何让connection来进行上下文的传递呢?

利用到ThreadLocal将数据库连接绑定到线程上下文是最好的使用方式。

所以如下所示:

1、从Spring事务管理器中获取得到连接connection;
2、利用connection.setAutoCommit(false);
3、将connection放入到ThreadLocal<DataSource,Connection>中;
4、target.executeSql();
5、connection.commit/rollback;

对于ThreadLocal来说,key是事务管理器中设置的数据源,value对应的才是数据库连接。如果利用key拿不到对应的数据库连接,那么对于jdbcTemplate或者是mybatisTempalte来说,是自己去建立一个数据库连接。

所以对于DataSource数据源来说,对Spring事务管理器、jdbcTemplate以及mybatisTemplate来说,设置的dataSource数据源要是同一个数据源

所以配置的时候是利用@Configuration来进行配置的。如下所示:

让数据源事务管理器中、JdbcTemplate中使用的是同一个数据源。

3.2、隔离级别和传播机制

情景一:单个事务

隔离级别是数据库层面的,Spring不会过多做什么事情;但是传播机制是Spring来进行控制的,在一个事务中再次来创建一个事务,同时创建多个数据库连接。如下所示:

@Transactional
public void a(){
  template.executeSql1();
  template.executeSql2();
  b();
}

@Transactional
public void b(){
  template.executeSql3();
}

那么对应的伪代码实现大致上如下所示:

1、从Spring事务管理器中获取得到连接connection;
2、利用connection.setAutoCommit(false);
3、将connection放入到ThreadLocal<DataSource,connection>中;
4、target.executeSql(); sql1、sql2、b()方法
5、connection.commit/rollback;
说明

上面的写法的目的是什么?看起来是两个@Transactional方法,但是真正发挥作用的是a()方法上的注解,创建的数据库连接也就只有一个而已。

在a()方法中调用b()方法,就相当于是目标对象.b()方法,而不是代理对象.b()方法。

情景二:多个事务

@Transactional
public void a(){
  template.executeSql1();
  template.executeSql2();
  currentProxy.b();
}

@Transactional
public void b(){
  template.executeSql3();
}

那么新开一个事务又是如何来做到的呢?用伪代码解释如下所示:

需要在执行b()方法之前,将ThreadLocal<DataSource,Connection>中的Connection保存起来,然后在b()方法执行完成之后,再设置到ThreadLocal<DataSource,Connection>中来。

1、从Spring事务管理器中获取得到连接connection;
2、利用connection.setAutoCommit(false);
3、将connection放入到ThreadLocal<DataSource,Connection>中;
4、target.executeSql(); sql1、sql2、
    b()方法
    4.0、挂起操作-->创建挂起对象,将之前connection设置到挂起对象中;
    4.1、从Spring事务管理器中获取得到连接newConnection;
	4.2、利用newConnection.setAutoCommit(false);
	4.3、将newConnection放入到ThreadLocal<DataSource,newConnection>中;
	4.4、target.executeSql(); 
	4.5、newConnection.commit/rollback,放入到ThreadLocal<Map>中来;
	4.6、将挂起的connection设置回去;
5、sql3
6、connection.commit/rollback;

新增了4.0和4.6步骤,再执行新事务的时候,先保存旧事务,在新的事务执行完成之后,恢复之前的旧事务。

证明一

那么通过实践证明一下,对应的配置类:

对应的Service类:

那么在连接数据库工具中,看一下当前数据库的连接数量:

show status like '%Threads_connected%';

在没有启动应用程序之前,当前显示的只有2个,下面来启动程序:

当前再次运行:

show status like '%Threads_connected%';

运行之后,发现现在有3个了。那么将断点放到下面一个去看下

再次执行

show status like '%Threads_connected%';

可以看到数据库是有4个的。那么验证了上面的逻辑是正确的。

证明二

那么来修改一下事务注解,也为@Transactional,不加任何传播行为:

那么再次来进行执行,打上断点

此时运行SQL,检查下

show status like '%Threads_connected%';

此时有3个,然后放行到下一个断点中来

此时运行SQL,检查下

show status like '%Threads_connected%';

此时还是只有3个。说明了什么问题?

说明了a()方法和b()方法都在用同一个数据库连接

四、使用Spring事务问题

情景一

根据场景来制造问题:

在b()方法中,抛出一个异常,再来尝试一下。发现数据库中没有任何修改变化。那么这种方式是可以理解的,因为b()方法抛出异常,而导致a()方法抛出异常,从而导致了事务回滚。

情景二

那么接下来,如果将b()方法在a()中调用的地方对异常进行捕捉呢?看下下面的代码:

在此来进行执行,发现数据库还是没有变化。然后看下控制台抛出的异常信息

发现抛出来的信息不同了。

从主观上理解来看,因为a()和b()方法共用一个数据库连接Connection且SQL都是在同一个数据库连接中执行的,那么不可能说a()方法中的SQL执行完成,而b()方法中的SQL执行失败了,这样子不符合事务原子性操作

那么到底是哪里来控制的呢?提前说一下

在DataSourceTransactionManager中有一个属性globalRollbackOnParticipationFailure用来表示:一个事务中部分失败了,要不要整个事务都回滚,对于上面的a()和b()方法来说,Spring认为即使是在同一个数据库连接中执行SQL,但是因为分为了两个方法来进行执行,因为一个方法失败了,Spring就认为整个事务是失败了的。那么如果修改一下呢?来尝试一下:

那么再次来执行下面的service

这次发现没有异常出现,SQL都执行成功了。因为事务的原子性保证了数据库中的同一组SQL要么全部执行成功,要么全部执行失败。

既然在事务管理器中设置了对于同一个数据库连接来说,部分事务失败不会导致整个事务不会滚这个属性之后,那么全部SQL都会来执行成功。

情景二:伪代码实现

伪代码如下所示:

1、从Spring事务管理器中获取得到连接connection;
2、利用connection.setAutoCommit(false);
3、将connection放入到ThreadLocal<DataSource,Connection>中;
4、target.executeSql(); sql1、sql2、
    b()方法
    4.1、拿到上面的数据库连接;
	4.2、target.executeSql(); 
	4.3、出现异常,回滚。但是并不是直接调用connection.rollback,而是在RollBackThreadLocal<Map>中添加个标记,
        记录b()方法失败了。假如说是rollback=true
5、sql3执行成功
6、a()方法对应的事务准备提交时,在提交过程中检查RollBackThreadLocal中的假如说是rollback标记是否为true,如果为true的话,表示应该来进行回滚;如果为false,那么表示应该进行提交。

所以说,很奇怪的是在事务提交的时候还需要检查下标记判断是否需要进行事务回滚

但是不得不说这个操作很六啊!但是很可惜的是,我们一般很少来修改DataSourceTransactionManager中的globalRollbackOnParticipationFailure变量的值。

所以说,大多数场景下,我们是遇不到上面的情况的!只会看到情景二中的抛出异常的情况。

思想总结

用伪代码来进行描述

1、从Spring事务管理器中获取得到连接connection;
2、利用connection.setAutoCommit(false);
3、将connection放入到ThreadLocal<DataSource,Connection>中;
4、target.executeSql(); sql1、sql2、
    b()方法
    4.1、拿到上面的数据库连接;
	4.2、target.executeSql(); 
	4.3、出现异常,回滚。但是并不是直接调用connection.rollback,而是在RollBackThreadLocal<Map>中添加个标记,
        记录b()方法失败了。假如说是rollback=true
5、sql3执行成功
6、a()方法对应的事务准备提交时,在提交过程中检查RollBackThreadLocal中的假如说是rollback标记是否为true,如果为true的话,表示应该来进行回滚;如果为false,那么表示应该进行提交。

因为传播行为导致了Spring在事务控制中进行了大量的逻辑判断,也就是上面伪代码的变种。变的是4.1,到底是复用当前线程中保存的线程还是说从事务管理器中重新获取得到数据库连接以及控制事务提交和回滚的操作。

五、Spring事务传播机制

在开发过程中,经常会出现一个方法调用另外一个方法,那么这里就涉及到了多种场景,比如a()调用b():

  1. a()和b()方法中的所有sql需要在同一个事务中吗?
  2. a()和b()方法需要单独的事务吗?
  3. a()需要在事务中执行,b()还需要在事务中执行吗?
  4. 等等情况...

所以,这就要求Spring事务能支持上面各种场景,这就是Spring事务传播机制的由来。那Spring事务传播机制是如何实现的呢?

先来看上述几种场景中的一种情况,a()在一个事务中执行,调用b()方法时需要新开一个事务执行:

  1. 首先,代理对象执行a()方法前,先利用事务管理器新建一个数据库连接a
  2. 将数据库连接a的autocommit改为false
  3. 把数据库连接a设置到ThreadLocal中
  4. 执行a()方法中的sql
  5. 执行a()方法过程中,调用了b()方法(注意用代理对象调用b()方法)
    1. 代理对象执行b()方法前,判断出来了当前线程中已经存在一个数据库连接a了,表示当前线程其实已经拥有一个Spring事务了,则进行挂起
    2. 挂起就是把ThreadLocal中的数据库连接a从ThreadLocal中移除,并放入一个挂起资源对象
    3. 挂起完成后,再次利用事务管理器新建一个数据库连接b
    4. 将数据库连接b的autocommit改为false
    5. 把数据库连接b设置到ThreadLocal中
    6. 执行b()方法中的sql
    7. b()方法正常执行完,则从ThreadLocal中拿到数据库连接b进行提交
    8. 提交之后会恢复所挂起的数据库连接a,这里的恢复,其实只是把在挂起资源对象中所保存的数据库连接a再次设置到ThreadLocal中
  6. a()方法正常执行完,则从ThreadLocal中拿到数据库连接a进行提交

这个过程中最为核心的是:在执行某个方法时,判断当前是否已经存在一个事务,就是判断当前线程的ThreadLocal中是否存在一个数据库连接对象,如果存在则表示已经存在一个事务了。

5.1、Spring事务传播机制分类

其中,以非事务方式运行,表示以非Spring事务运行,表示在执行这个方法时,Spring事务管理器不会去建立数据库连接,执行sql时,由Mybatis或JdbcTemplate自己来建立数据库连接来执行sql。

案例分析

情况1

@Component
public class UserService {
  @Autowired
  private UserService userService;

  @Transactional
  public void test() {
    // test方法中的sql
    userService.a();
  }

  @Transactional
  public void a() {
    // a方法中的sql
  }
}

默认情况下传播机制为REQUIRED,表示当前如果没有事务则新建一个事务,如果有事务则在当前事务中执行。


所以上面这种情况的执行流程如下:

  1. 新建一个数据库连接conn
  2. 设置conn的autocommit为false
  3. 执行test方法中的sql
  4. 执行a方法中的sql
  5. 执行conn的commit()方法进行提交

情况2

假如是这种情况

@Component
public class UserService {
  @Autowired
  private UserService userService;

  @Transactional
  public void test() {
    // test方法中的sql
    userService.a();
    int result = 100/0;
  }

  @Transactional
  public void a() {
    // a方法中的sql
  }
}

所以上面这种情况的执行流程如下:

  1. 新建一个数据库连接conn
  2. 设置conn的autocommit为false
  3. 执行test方法中的sql
  4. 执行a方法中的sql
  5. 抛出异常
  6. 执行conn的rollback()方法进行回滚,所以两个方法中的sql都会回滚掉

情况3

假如是这种情况:

@Component
public class UserService {
  @Autowired
  private UserService userService;

  @Transactional
  public void test() {
    // test方法中的sql
    userService.a();
  }

  @Transactional
  public void a() {
    // a方法中的sql
    int result = 100/0;
  }
}

所以上面这种情况的执行流程如下:

  1. 新建一个数据库连接conn
  2. 设置conn的autocommit为false
  3. 执行test方法中的sql
  4. 执行a方法中的sql
  5. 抛出异常
  6. 执行conn的rollback()方法进行回滚,所以两个方法中的sql都会回滚掉

情况4

如果是这种情况:

@Component
public class UserService {
  @Autowired
  private UserService userService;

  @Transactional
  public void test() {
    // test方法中的sql
    userService.a();
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void a() {
    // a方法中的sql
    int result = 100/0;
  }
}

所以上面这种情况的执行流程如下:

  1. 新建一个数据库连接conn
  2. 设置conn的autocommit为false
  3. 执行test方法中的sql
  4. 又新建一个数据库连接conn2
  5. 执行a方法中的sql
  6. 抛出异常
  7. 执行conn2的rollback()方法进行回滚
  8. 继续抛异常,对于test()方法而言,它会接收到一个异常,然后抛出
  9. 执行conn的rollback()方法进行回滚,最终还是两个方法中的sql都回滚了

Spring事务强制回滚

正常情况下,a()调用b()方法时,如果b()方法抛了异常,但是在a()方法捕获了,那么a()的事务还是会正常提交的,但是有的时候,我们捕获异常可能仅仅只是不把异常信息返回给客户端,而是为了返回一些更友好的错误信息,而这个时候,我们还是希望事务能回滚的,那这个时候就得告诉Spring把当前事务回滚掉,做法就是:

@Transactional
public void test(){

  // 执行sql
  try {
    b();
  } catch (Exception e) {
    // 构造友好的错误信息返回
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  }
}

public void b() throws Exception {
  throw new Exception();
}

TransactionSynchronization

Spring事务有可能会提交,回滚、挂起、恢复,所以Spring事务提供了一种机制,可以让程序员来监听当前Spring事务所处于的状态。

@Component
public class UserService {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  @Autowired
  private UserService userService;

  @Transactional
  public void test(){
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {

      @Override
      public void suspend() {
        System.out.println("test被挂起了");
      }

      @Override
      public void resume() {
        System.out.println("test被恢复了");
      }

      @Override
      public void beforeCommit(boolean readOnly) {
        System.out.println("test准备要提交了");
      }

      @Override
      public void beforeCompletion() {
        System.out.println("test准备要提交或回滚了");
      }

      @Override
      public void afterCommit() {
        System.out.println("test提交成功了");
      }

      @Override
      public void afterCompletion(int status) {
        System.out.println("test提交或回滚成功了");
      }
    });

    jdbcTemplate.execute("insert into t1 values(1,1,1,1,'1')");
    System.out.println("test");
    userService.a();
  }

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void a(){
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {

      @Override
      public void suspend() {
        System.out.println("a被挂起了");
      }

      @Override
      public void resume() {
        System.out.println("a被恢复了");
      }

      @Override
      public void beforeCommit(boolean readOnly) {
        System.out.println("a准备要提交了");
      }

      @Override
      public void beforeCompletion() {
        System.out.println("a准备要提交或回滚了");
      }

      @Override
      public void afterCommit() {
        System.out.println("a提交成功了");
      }

      @Override
      public void afterCompletion(int status) {
        System.out.println("a提交或回滚成功了");
      }
    });

    jdbcTemplate.execute("insert into t1 values(2,2,2,2,'2')");
    System.out.println("a");
  }
}

六、源码分析

源码很难,非常的复杂。不方便来进行分析,画图来进行表示吧

放上两个参考链接:

第一个

第二个

七、Spring事务两种用法

Spring支持编程式事务管理和声明式事务管理两种方式。

编程式事务管理使用transactiontemplate或者直接使用底层的platformtransactionmanager。对于编程式事务管理,Spring推荐使用transactiontemplate。

7.1、分析

我们知道@Transactional 注解,是使用 AOP 实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。当 Spring 遇到该注解时,会自动从数据库连接池中获取 connection,并开启事务然后绑定到 ThreadLocal 上,对于@Transactional注解包裹的整个方法都是使用同一个connection连接 。

如果我们出现了耗时的操作,比如第三方接口调用,业务逻辑复杂,大批量数据处理等就会导致我们我们占用这个connection的时间会很长,数据库连接一直被占用不释放。一旦类似操作过多,就会导致数据库连接池耗尽。

在一个事务中执行RPC操作导致数据库连接池撑爆属于是典型的长事务问题 ,类似的操作还有在事务中进行大量数据查询,业务规则处理等...

长事务

顾名思义就是运行时间比较长,长时间未提交的事务,也可以称之为大事务 。

长事务会引发哪些问题?

长事务引发的常见危害有:

  1. 数据库连接池被占满,应用无法获取连接资源;
  2. 容易引发数据库死锁;
  3. 数据库回滚时间长;
  4. 在主从架构中会导致主从延时变大。

如何避免长事务发生

既然知道了长事务的危害,那如何在开发中避免出现长事务问题呢?

很明显,解决长事务的宗旨就是 对事务方法进行拆分,尽量让事务变小,变快,减小事务的颗粒度。

既然提到了事务的颗粒度,我们就先回顾一下Spring进行事务管理的方式。

7.2、声明式事务

首先我们要知道,通过在方法上使用@Transactional注解进行事务管理的操作叫声明式事务

使用声明式事务的优点 很明显,就是使用很简单,可以自动帮我们进行事务的开启、提交以及回滚等操作。

使用这种方式,程序员只需要关注业务逻辑就可以了。

声明式事务有一个最大的缺点 ,就是事务的颗粒度是整个方法,无法进行精细化控制。

使用声明式事务条件

@Transactional使用这么简单,有没有办法既可以使用@Transactional,又能避免产生长事务

解决办法:那就需要对方法进行拆分,将不需要事务管理的逻辑与事务操作分开:

@Service
public class OrderService{

  public void createOrder(OrderCreateDTO createDTO){
    query();
    validate();
    saveData(createDTO);
  }

  //事务操作
  @Transactional(rollbackFor = Throwable.class)
  public void saveData(OrderCreateDTO createDTO){
    orderDao.insert(createDTO);
  }
}

uery()validate()不需要事务,我们将其与事务方法saveData()拆开。

当然,这种拆分会命中使用@Transactional注解时事务不生效的经典场景,很多新手非常容易犯这个错误。@Transactional注解的声明式事务是通过Spring aop起作用的,而Spring aop需要生成代理对象,直接在同一个类中方法调用使用的还是原始对象,事务不生效。其他几个常见的事务不生效的场景为:

  • @Transactional 应用在非 public 修饰的方法上
  • @Transactional 注解属性 propagation 设置错误
  • @Transactional 注解属性 rollbackFor 设置错误
  • 同一个类中方法调用,导致@Transactional失效
  • 异常被catch捕获导致@Transactional失效

正确的拆分方法应该使用下面两种:

1、自己注入自己

@Service
public class OrderService{

  @Autowired
  private OrderService orderService;

  public void createOrder(OrderCreateDTO createDTO){
    query();
    validate();
    orderService.saveData(createDTO);
  }

  //事务操作
  @Transactional(rollbackFor = Throwable.class)
  public void saveData(OrderCreateDTO createDTO){
    orderDao.insert(createDTO);
  }
}

2、配置类上添加@EnableAspectJAutoProxy(exposeProxy = true),方法内使用AopContext.currentProxy()获得代理类,使用事务。

// 开启AOP
@EnableAspectJAutoProxy(exposeProxy=true)
@Configuration
public class LiAppCOnfig {}

对应的类就可以写上

@Service
public class OrderService{

  public void createOrder(OrderCreateDTO createDTO){
    query();
    validate();
    OrderService orderService = (OrderService)AopContext.currentProxy();
    orderService.saveData(createDTO);
  }

  //事务操作
  @Transactional(rollbackFor = Throwable.class)
  public void saveData(OrderCreateDTO createDTO){
    orderDao.insert(createDTO);
  }
}

使用声明式事务出现问题

Spring中事务+Lock锁出现的问题。代码如下所示:

主要问题在于lock锁添加的位置。

因为在lock锁生效之前,数据库连接已经从事务管理器中获取得到了数据库连接了。然后lock锁在进行释放之后,数据库连接才进行了关闭。

所以可以修改一下,如下所示:

7.3、编程式事务

与声明式事务对应的就是编程式事务 ,基于底层的API,开发者在代码中手动的管理事务的开启、提交、回滚等操作。在Spring项目中可以使用TransactionTemplate类的对象,手动控制事务。

使用编程式事务最大的好处就是可以精细化控制事务范围,所以避免长事务最简单的方法就是不要使用声明式事务@Transactional,而是使用编程式事务手动控制事务范围。

使用编程式事务

对应的是org.Springframework.transaction.support.TransactionTemplate类。

可以看下这个类的结构,发现就是事务定义对象,即类似对@Transactional中解析得到的信息对象。

分别看下TransactionTemplate实现的两个接口:TransactionOperations(如何操作事务)、InitializingBean(初始化回调方法)

TransactionOperations接口

public interface TransactionOperations {

  // 在事务中以指定的动作来进行运行
  @Nullable
  <T> T execute(TransactionCallback<T> action) throws TransactionException;

  // 执行事务没有结果
  default void executeWithoutResult(Consumer<TransactionStatus> action) throws TransactionException {
    execute(status -> {
      action.accept(status);
      return null;
    });
  }

  static TransactionOperations withoutTransaction() {
    return WithoutTransactionOperations.INSTANCE;
  }

}

InitializingBean接口

既然TransactionTemplate实现了InitializingBean接口,那么看一下这里的afterPropertiesSet方法

@Override
public void afterPropertiesSet() {
  if (this.transactionManager == null) {
    throw new IllegalArgumentException("Property 'transactionManager' is required");
  }
}

在TransactionTemplate中只有唯一一个属性

private PlatformTransactionManager transactionManager;

说明了Spring想让我们在配置TransactionTemplate成为一个bean的时候,给属性赋值上一定要赋值事务管理器。

继承DefaultTransactionDefinition类

public class DefaultTransactionDefinition implements TransactionDefinition, Serializable {
  // 事务传播行为
  public static final String PREFIX_PROPAGATION = "PROPAGATION_";

  // 事务隔离级别
  public static final String PREFIX_ISOLATION = "ISOLATION_";

  // 隔离级别
  public static final String PREFIX_TIMEOUT = "timeout_";

  // 数据库连接只读
  public static final String READ_ONLY_MARKER = "readOnly";

  /** Constants instance for TransactionDefinition. */
  static final Constants constants = new Constants(TransactionDefinition.class);

  // 以下是默认行为
  private int propagationBehavior = PROPAGATION_REQUIRED;

  private int isolationLevel = ISOLATION_DEFAULT;

  private int timeout = TIMEOUT_DEFAULT;

  private boolean readOnly = false;

  @Nullable
  private String name;
  ......
}

看到了事务管理器和事务属性,那么不由得想到在源码中看到的一行代码:

根据事务属性决定开启事务,返回DefaultTransactionStatus(事务状态),可以根据事务状态来在代码中进行调整。

代码如下:

@Service
public class AccountServiceImpl implements AccountService {
  
  @Autowired
  private AccountDao accountDao;
  @Autowired
  private TransactionDefinition txDefinition;
  @Autowired
  private PlatformTransactionManager txManager;
  
  // 手动开启事务
  @Override
  public void transfer(String from, String to, Float money) {
    //开启事务,得到事务状态
    TransactionStatus txStatus = txManager.getTransaction(txDefinition);
    try {      
      Account fromAccount = accountDao.findByName(from);
      Account toAccount = accountDao.findByName(to);
      fromAccount.setMoney(fromAccount.getMoney() - money);
      toAccount.setMoney(toAccount.getMoney() + money);
      accountDao.edit(fromAccount);
      accountDao.edit(toAccount);
      //提交事务
      txManager.commit(txStatus);
    } catch (Exception e) {
      //回滚事务
      txManager.rollback(txStatus);
      e.printStackTrace();
    }
  }
}

或者直接简写如下所示:

而对于属性来说,都使用默认的配置。然后在调用的时候使用:

正常情况下,可以这么来进行使用。

那么看一下异常情况:

1、execute里如果不加try...catch... ,异常后可以回滚,但是会向外抛出异常,不会走后续代码

2、加上try catch后,必须在catch中手动加上status.setRollbackOnly(),否则无法回滚数据

那么这段代码在声明式事务中有一行代码特别相似

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

而在TransactionTemplate中,已经有了TransactionStatus,那么这么来进行设置可以更加方便的来进行设置。

所以编程式事务也是非常实用的。

TransactionTemplate.execute源码分析

源码如下所示:

首先映入眼帘的就是熟悉的代码:根据事务属性来从事务管理器中获取得到事务状态。作为参数传递到lambda表达式中去,那么可以在用户自定义的方式中来进行使用。然后就是回滚||提交代码。

还可以自定义用户指定的同步方法等,如下:

public void executeSql(){
  Boolean execute = transactionTemplate.execute(status -> {
    jdbcTemplate.execute("insert into user (username,password) values ('ligang','654321')");
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
      @Override
      public void beforeCommit(boolean readOnly) {

      }
      @Override
      public void beforeCompletion() {

      }

      @Override
      public void afterCommit() {
        jdbcTemplate.execute("delete from user where id  = 5");
      }

      @Override
      public void afterCompletion(int status) {

      }
    });
    return Boolean.TRUE;
  });
}

在对应的动作触发之后,这里也会来进行执行。

posted @ 2022-06-27 00:12  雩娄的木子  阅读(174)  评论(0编辑  收藏  举报