@Transactional的那些事

1,067 阅读7分钟

在springboot中我们经常使用@Transactional注解来做数据库的事务,虽然经常使用但是对于他的内部属性却是很陌生,所以今天我们来看看@Transactional注解到底可以做到那些事情。

打开Transactional这类,我们可以看到其中有诸多属性如下:

public @interface Transactional {

	@AliasFor("transactionManager")
	String value() default "";

	@AliasFor("value")
	String transactionManager() default "";

	Propagation propagation() default Propagation.REQUIRED;

	Isolation isolation() default Isolation.DEFAULT;

	boolean readOnly() default false;

	String[] rollbackForClassName() default {};

	String[] noRollbackForClassName() default {};

}

  • value和transactionManager

    首先要说的是在value和transactionManager上面有一个注解@AliasFor,这个注解的意思是某个属性的别名。所以在使用的时候value和transactionManager达到的效果是一样的。那么这两者要达到的效果究竟为何呢?

    在我们开发中,有可能会使用到多数据源的情况。那么在此时我们可能需要在使用不同的数据源时使用不同的事务管理。这个时候可以使用这两个属性去指定所使用的事务管理器。

    // 配置不同的事务管理器可以使用xml配置文件配置,亦可以使用java类配置
    <tx:annotation-driven/>    
        
    <bean id="transactionManager1" class="org.springframework.jdbc.DataSourceTransactionManager">    
    ...     
    <qualifier value="a"/>    
    </bean>    
    <bean id="transactionManager2" class="org.springframework.jdbc.DataSourceTransactionManager">    
    ...     
    <qualifier value="b"/>    
    </bean>  
    
    // 此处着重说明数据源管理器,所以数据库配置只是随手放进去的
    @Configuration
    @MapperScan(basePackages = "com.itmayiedu.test02", sqlSessionFactoryRef = "test2SqlSessionFactory")
    public class DataSource2Config {
    
        @Bean(name = "test2DataSource")
        @ConfigurationProperties(prefix = "spring.datasource.test2")
        public DataSource testDataSource() {
            return DataSourceBuilder.create().build();
        }
    
        @Bean(name = "test2SqlSessionFactory")
        public SqlSessionFactory testSqlSessionFactory(@Qualifier("test2DataSource") DataSource dataSource)
                throws Exception {
            SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
            bean.setDataSource(dataSource);
            return bean.getObject();
        }
    
        // 此处配置数据源管理器
        @Bean(name = "test2TransactionManager")
        public DataSourceTransactionManager testTransactionManager(@Qualifier("test2DataSource") DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    
        @Bean(name = "test2SqlSessionTemplate")
        public SqlSessionTemplate testSqlSessionTemplate(
                @Qualifier("test2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
            return new SqlSessionTemplate(sqlSessionFactory);
        }
    
    }
    
  • propagation属性(事务的传播行为)

    事务的传播行为。所谓事务的传播行为,是指在当前事务开启之前,已经有一个事务上下文存在了的情况(比如说我们给方法A和方法B分别加上了事务,然后在A中调用了B,这时就需要选择事务传播行为,使A做出期待的应对。

    该属性包含的可选项如下 :

    • REQUIRED(默认):
      • 若方法A调用时,上下文中存在事务,则加入当前事务
      • 若方法A调用时,上下文中没有事务,则新建一个事务
      • 当在方法A调用方法B时,方法A、B将使用同一个事务
      • 如果方法B发生异常需要回滚时,将回滚整个事务
    • REQUIRED_NEW :
      • 对于方法A与B,无论方法调用时是否存在事务,都会开启一个新的事务
      • 且如果方法调用时存在事务,当前事务将被延缓
      • 如果方法B发生异常,将不会导致方法A的回滚
    • NESTED :
      • 与REQUIRED_NEW类似,但支持JDBC,不支持JPA或Hibernate
    • SUPPORTS :
      • 如果方法调用时,上下文中已经开启了事务,就使用这个事务
      • 如果方法调用时,上下文中没有事务,就不使用事务
    • NOT_SUPPORTS :
      • 以非事务的方式运行,如果存在事务,在方法调用到结束阶段事务都将被挂起
    • NEVER :
      • 以非事务的方式运行,如果存在事务,则将抛出异常
    • MANDATORY :
      • 强制方法在事务中执行,如果存在事务,则加入当前事务
      • 如果不存在事务,则将抛出异常
  • isolation

    事务的隔离级别。所谓事务的隔离级别,是指若干个并发的事务之间的隔离程度,由低到高依次分别为 :read uncommited(读未提交)、read commited(读提交)、read repeatable(读重复)、serializable(序列化);这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。

    • DEFAULT

      使用数据库的默认隔离级别

    • READ_UNCOMMITTED

      即读未提交。允许在提交该行中的任何更改之前,由一个事务更改的行被另一事务读取,可导致脏读,不可重复读,以及幻读

    • READ_COMMITTED

      即读提交。只防止脏读,可能会发生不可重复读和幻读。此级别仅禁止事务读取其中包含未提交的更改的行。

    • REPEATABLE_READ

      即读重复,防止脏读和不可重复读;可能会发生幻读。此级别禁止事务读取行中未提交更改的行,并且还禁止以下情况:一个事务读取一行,第二个事务更改该行,第一个事务重新读取该行,第二次获得不同的值

    • SERIALIZABLE

      即序列化,防止脏读,不可重复读和幻像读。该级别包括重复读中的禁止条件,并且进一步禁止以下情况:一个事务读取满足某个条件的所有行,第二个事务插入满足这个条件的行,并且对于相同条件,第一个事务将重新读取,并在第二个读取中检索其他“幻像”行。

  • timeout :

    事务的过期时间。默认为当前数据库的事务过期时间

  • readonly :

    指定当前事务是否为只读事务,默认为false

  • rollbackFor :

    指定哪个或哪些异常发生时,需要引起事务回滚,默认为Throwable的子类

  • noRollBackFor :

    指定哪个或哪些异常发生时,不引起事务回滚

注意:@Transaction只有应用到public方法才能有效

在应用系统调用声明 @Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,并在代码运行时生成一个代理对象根据 @Transaction 的属性配置信息,这个代理对象将决定该目标方法是否由拦截器 TransactionInterceptor 来拦截TransactionInterceptor在拦截时,首先会在目标方法开始执行之前创建并加入事务,然后执行目标方法,根据执行情况是否出现异常,利用抽象事务管理器 AbstarctPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务。

CglibAopProxy 的一个内部类中的 intercept 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法。这个方法会检查目标方法的修饰符是否 public,如果不是,就不会获取 @Transaction 的配置信息,最终造成不会使用 TransactionInterceptor 来拦截改目标方法,从而导致事务管理不被进行。

避免 Spring 中的 AOP 自调用问题

其实这个是是AOP的嵌套原则造成的,其具体规则如下:

a.调用方法不满足拦截规则,调用本类中其他满足拦截条件的方法,这个时候不会启动aop的拦截方法 b.调用方法不满足拦截规则,调用其他类中满足拦截条件的方法,这个时候会启动aop拦截方法 c.调用满足拦截的方法a再调用满足拦截的方法b(分为在本类内和在类外) 在类外的会启动aop的拦截,拦截的顺序是先进入a方法的后置通知然后进入b方法后置,走完b方法的通知之后再进入a方法的后置通知,然后走完a方法的通知 ,如果是在本类内调用的只会执行一次aop

  • 嵌套原则的形成因素

    因为aop拦截的并不是真正的目标类而是注入ioc容器的代理类,但是在java中如果调用方法则是使用this.method()这种形式去调用,此时this所指向的并不是代理类而是类的本身。所以这个时候aop并不能拦截到方法。

具体得解决办法有三种

  1. 自己注入到自己

    @Component
    public class TestAopService {
        @Resource
        private TestAopService testAopService;
        public TestAopService() {
        }
        @TestAnnotation
        public void say1(){
            testAopService.say2();
            System.out.println("1");
        }
        @TestAnnotation
        public void say2(){
            System.out.println("1");
        }
    }
    
  2. 使用AopContext.currentProxy()

    @Component
    public class TestAopService {
        public TestAopService() {
        }
    
        @TestAnnotation
        public void say1(){
            ((TestAopService)AopContext.currentProxy()).say2();
            System.out.println("1");
        }
        @TestAnnotation
        public void say2(){
            System.out.println("1");
        }
    }
    

    但是要注意的是用AopContext.currentProxy()必须要搭配注解@EnableAspectJAutoProxy(exposeProxy = true)使用,因为AopContext扫描bean的功能默认是关闭的,必须要手动设置为true才可以

  3. 使用ApplicationContext查找bean

    @Component
    public class TestAopService {
        @Autowired
        private ApplicationContext applicationContext;
        public TestAopService() {
        }
    
        @TestAnnotation
        public void say1(){
            TestAopService bean = applicationContext.getBean(TestAopService.class);
            bean.say2();
            System.out.println("1");
        }
        @TestAnnotation
        public void say2(){
            System.out.println("1");
        }
    }