@Transactional使用及原理

1,894 阅读11分钟

本文主要围绕为什么一个@Transactional注解,就可以使我们的方法增强为事务方法引申出三个问题:

1: Spring的事务与数据库的事务有什么关系? 2: Spring是如何实现事务的 3:@Transactional是如何简化Spring事务的配置?

事务

定义事务

在Spring中通过TransactionDefition来存储事务的定义:

public interface TransactionDefinition {
    // 事务的传播机制分类(主要用于定义一个事务方法嵌套另一个事务方法时的行为)
    int PROPAGATION_REQUIRED = 0;// 默认传播机制,有事务则加入,无则新建
    int PROPAGATION_SUPPORTS = 1; // 存在事务则加入,不存在事务则直接执行
    int PROPAGATION_MANDATORY = 2;// 强制要求调用方法中存在事务,如果不存在则直接抛出异常
    int PROPAGATION_REQUIRES_NEW = 3;// 每次都新建一个事务,并于当前事务不共享事务状态,即新建事务执行失败回滚后,原事务无需回滚。
    int PROPAGATION_NOT_SUPPORTED = 4;// 不支持当前事务,前事务会被挂起
    int PROPAGATION_NEVER = 5; // 如果调用方法中存在事务则直接抛出异常
    int PROPAGATION_NESTED = 6; //嵌入事务,与前事务共享一个事务状态
    // 事务的隔离机制分类
    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1; // 未提交读
    int ISOLATION_READ_COMMITTED = 2; // 提交读
    int ISOLATION_REPEATABLE_READ = 4; // 可重复读
    int ISOLATION_SERIALIZABLE = 8; // 序列化(完全满足ACID特性)
    // 事务的超时机制
    int TIMEOUT_DEFAULT = -1;
    // 获取当前事务的传播机制
    int getPropagationBehavior();
    // 获取当前事务的隔离机制
    int getIsolationLevel();
    // 获取当前事务的超时时间
    int getTimeout();
    // 当前事务是否只读? //TODO
    boolean isReadOnly();
    // 获取当前事务的名称
    String getName();
}

事务分类:

局部事务:

当前事务只涉及到一台机器上的一个数据库:一般是基于 一个数据库连接,而数据库也可以基于一个Connection(连接)层面实现数据库事务。

全局事务:

在分布式场景中,当前事务设计到多个数据库;不再是一个数据库连接的问题,涉及到保证分布式事务正确执行的一些算法,如2PC、3PC等

所以Spring的事务是数据库事务的组合与增强;那么Spring是如何实现事务的呢?

我们从最基础的JDBC事务谈起:

JDBC实现事务

    public void getDataByJdbc(Long userId) {
        try {
           // dataSource为配置的Mysql数据源
           Connection connection = dataSource.getConnection();
           // 关闭事务自动提交,手动设置事务范围
           connection.setAutoCommit(false);
           UserDTO userDTO = userService.findByUid(userId);
           // 手动提交事务
            connection.commit();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

但是如果此时我们需要更换HibernateMybatis进行数据访问时,所有涉及到事务提交的方法都需要进行改变:如使用Hibernate时,我们则需要使用基于Session的数据库连接方式,即我们的业务代码与具体的数据访问方式的事务实现方法紧密耦合在一起,并没有隔离开。

因此我们需要在数据访问方式与事务管理方式之间增加一层抽象-PlatformTransactionManger(PTM)

TransactionManger

增加了TM的抽象后,可以使用面向对象中传统的编程方式-面向接口编程,我们的代码依赖于TM接口,而在配置Bean时选择set相应数据库访问方式的具体接口实现类。

此时代码变为:

    @Autowired
    PlatformTransactionManager platformTransactionManager;
    
    public void getDataByPtm(Long userId) {
        // 声明当前事务属性: 事务的隔离级别为可重复读
        DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
        transactionDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
        // 通过事务属性生成当前事务
        TransactionStatus transactionStatus = platformTransactionManager.getTransaction(transactionDefinition);
        try {
            // 具体业务逻辑
            UserDTO userDTO = userService.findByUid(userId);
        } catch (Exception te) {
            // 有异常时直接回滚
            platformTransactionManager.rollback(transactionStatus);
        }
        // 最后进行事务的提交
        platformTransactionManager.commit(transactionStatus);
    }

由此可见,我们的业务代码与具体的数据访问方式实现事务的方法隔离开,代码编写时只需要关心事务属性和事务边界的定义即可,即使更换了数据访问方式,也只需要更改PlatformTransactionManager的配置即可。

模板模式进行代码优化

我们应该何时使用设计模式?当我们察觉到我们代码中的变化时,找到变化并使用设计模式封装变化。 变化:在上面的代码中,唯一变化的点在于我们的业务代码与异常提交,因此我们可以使用模板方法+callback的方式,使用模板方法复用事务状态的流转过程,而使用callback对外暴露进行业务处理,这样就很清楚的将代码中变化的点与不变的点隔离开。

    public void getSomeByPtmTemplate(Long uid) {
        TransactionTemplate template = new TransactionTemplate();
        // 使用具体的业务代码实现函数式接口方法 doInTransaction()
        template.execute(transactionCallback -> userService.findByUid(uid));
    }
    
    @FunctionalInterface
    public interface TransactionCallback<T> {
    	T doInTransaction(TransactionStatus var1);
    }

对应的Template方法中控制基本不会发生变化的事务状态流转:

     public <T> T execute(TransactionCallback<T> action) throws TransactionException {
        if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
            return ((CallbackPreferringPlatformTransactionManager)this.transactionManager).execute(this, action);
        } else {
            // 开启一个事务
            TransactionStatus status = this.transactionManager.getTransaction(this);

            Object result;
            try {
                // Lambda表达式即实现了函数式接口的这个方法
                result = action.doInTransaction(status);
            } catch (RuntimeException var5) {
                // 回滚当前事务
                this.rollbackOnException(status, var5);
                throw var5;
            } catch (Error var6) {
                this.rollbackOnException(status, var6);
                throw var6;
            } catch (Throwable var7) {
                this.rollbackOnException(status, var7);
                throw new UndeclaredThrowableException(var7, "TransactionCallback threw undeclared checked exception");
            }
            // 提交当前事务
            this.transactionManager.commit(status);
            return result;
        }
    }

使用AOP进行优化

我们可以比使用模板模式更进一步,使用AOP的方式进行优化,因为当我们使用模板模式时,我们仍不可避免的在我们的代码中依赖TransactionTemplate,并且我们察觉到事务管理的相关代码,符合切面的定义,整个代码可以分为:1、通过PTM开启事务 2、执行业务代码 3、关闭当前事务,因此我们可以将1、3步放入方法的切面逻辑中:

  1. 通过XML配置基于JDK动态代理或CGlib的ProxyFactoryBean并注入由PointCut、Advice组成的Advisor和被代理类(此处即为getData...()方法所属的Bean)以实现对应的事务切面逻辑:
    <bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        // 被代理的类
        <property name="target">
            <bean class="com.blog.UserServiceImpl"/>
        </property>
        // target类实现的接口,未实现接口或显示定义ProxyFactory的proxy-TargetClass、optimize属性时使用CGlib,否则使用JDK动态代理的AOP实现方式
        <property name="proxyInterfaces">
            <value>com.blog.UserService</value>
        </property>
        // Advisor,这里使用了Spring框架内部提供的事务Advisor
        <property name="interceptorNames">
            <list>
         <value>org.springframework.transaction.interceptor.TransactionInterceptor</value>
            </list>
        </property>
    </bean>

配置完成后,我们只需要在业务代码中注入userServiceProxy,并调用UserServiceImpl中被@Transactional修饰的接口方法时,默认会调用TransactionInterceptor中的事务切面逻辑完成被代理方法的事务增强。

  1. 我们也可以通过@Aspect、@PointCut、@Around实现一个对被@Transactional修饰的类或方法,进行事务切面逻辑增添。

在Spring框架中真正使用到@Transactional时,我们并没有显式的定义Aspect,但具体原理亦是如此,依据事务的advisor和被符合条件的被代理类(被@Tramsactional修饰)生成对应的ProxyFactoryBean,只不过这一步Spring框架已经帮我们完成了。

至此,我们的局部事务的实现已经比较成熟了并且比较接近Spring中的实现方法了,是时候分析如何在Spring中使用了。

Spring中使用事务(着重介绍@Transactional)

声明式(使用XML或注解等元数据)

使用声明式的方式,可以便于使用SpringAOP对于事务这种切面逻辑进行优化。

首先获取元数据信息,并增加相应的切面逻辑

1. XML

当我们选择在XML中进行元数据的配置时,我们可以通过以下方式对元数据进行获取并生成代理对象:

ProxyFactory

首先我们需要在我们的代码调用getData()时,通过反射获取在注解中定义的各种属性值;而因为getData()是Controller层代码,该Controller类并没有实现接口,所以ProxyFactory会使用CGlib,即CglibProxyFactory的方式,在拦截器(MethodInterceptor())中配置具体的事务切面逻辑,切点即为当前方法getData(),最后通过生成相应的代理类对外返回;也因为使用了CGlib,所以被代理的事务方法不可以是private的。

ProxyFactoryBean(ProxyFactory + FactoryBean)

当然我们也可以直接使用Spring提供的ProxyFactoryBean的方式,即Proxy FactoryBean,意为生成代理类的FactoryBean;我们只需要将具体的PointCutAdvice组成的Advisor放入FactoryBean中即可通过ProxyFactoryBean.getObject()的方法获取对应的代理类。

    <bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        // 被代理的类
        <property name="target">
            <bean class="com.blog.UserServiceImpl"/>
        </property>
        // target类实现的接口,未实现接口或显示定义ProxyFactory的proxy-TargetClass、optimize属性时使用CGlib,否则使用JDK动态代理的AOP实现方式
        <property name="proxyInterfaces">
            <value>com.blog.UserService</value>
        </property>
        // Advisor,这里使用了Spring框架内部提供的事务Advisor
        <property name="interceptorNames">
            <list>
         <value>org.springframework.transaction.interceptor.TransactionInterceptor</value>
            </list>
        </property>
    </bean>

但是当我们使用ProxyFactoryBean进行配置时,因为Spring AOP是基于JDK动态代理和CGlib,因此我们的ProxyFactoryBean的配置是与具体的BeanName耦合在一起的(想一想Proxy.newInstance的输入参数为:classLoader, class<?>, invocationHandler),因此我们对于每一个Bean,都需要配置一个对应的ProxyFactoryBean,并使用该代理BeanFactory获取我们需要的增强对象,着实麻烦;因此我们可以更进一步,使用Spring提供的代理类自动生成机制:

AbstractAutoProxyCreator

AbstractAutoProxyCreator的实现机制为在Bean的生命周期中,在Bean实例化、初始化的过程中,会调用BeanPostProcessor接口的方法,而AbstractAutoProxyCreator则实现了其子接口InstantiationAwareBeanPostProcessor,实现该接口后,会直接进行实例的构建并直接返回,而不会走完正常的实例实例化流程,因此我们只需要在该步骤时返回对应的ProxyFactoryBean即可,这样通过SpringIOC获取的所有该类实例的引用皆为包含了切面逻辑的ProxyFactoryBean的引用,不再需要通过XML的配置形式进行BeanNameProxyFactoryBean的一一对应。

因此我们需要在BeanPostProcessor接口的postProcessAfterInitialization方法中对原有的实例化即将完成的Bean进行切面的增强,并通过XML或注解形式获取对应Bean中被@Transactional修饰的方法中的事务属性等元数据信息,用于自动生成该Bean所对应的ProxyFactoryBean并返回,这样我们调用的所有实例都含有了事务的切面逻辑。(具体方法下文分析注解实现方式时会一同分析)

2. 注解

    // 事务的隔离级别为可重复读,并且当有任何异常时,直接回滚
    @Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
    public void getDataByTransactional(Long uid) {
        UserDTO userDTO = userService.findByUid(uid);
    }

并在配置文件中增加:

// JDBC数据访问方式对应的PlatformTransacationManager
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

// 支持注解驱动
    <tx:annotation-driven transaction-manager="transactionManager"/>

可见,使用声明式编程的方式,更加简洁明了,但是其本质仍然与编码式无异,下面分析具体原理。

解析XML配置文件,并在Spring容器中注册事务的切面(advisor)

   <tx:annotation-driven transaction-manager="transactionManager"/>

首先,Spring容器在启动时,会通过XmlBeanDefinitionReader类 解析XML文件,然后解析过程中遇到<tx:>这个Spring自定义的命名空间(nameSpace),在Spring中通过NameSpaceHandler接口相应的实现类对自定义的命名空间进行解析,而tx对应的命名空间为TxNameSpaceHandler,Spring容器通过一个Map存储<命名空间,命名空间Handler>的映射关系,通过parseContext存储当前XmlBeanDefinitionReader类对XML解析的上下文。

tx命名空间中有annotation-driven等三个子标签,而每个子标签在Spring中会对应一个parser,在Spring容器取得TxNameSpaceHandler的实例后,调用其init方法将对应的parser加载进Spring容器中,并在Handler中维护<子标签,parser>的映射关系(如:<"annotation-driven", AnnotationDrivenBeanDefinitionParser.Class>),当XMLReader解析至annotation-driven时,会调用AnnotationDrivenBeanDefinitionParser类的parse()方法对命名空间中标签的属性进行相应的解析,同时会通过其静态内部类中的AopAutoProxyConfigure()方法向Spring容器中注册TransactionDefinition(事务属性,pointCut等信息)TransactionInterceptor(事务的advice)以及 BeanFactoryTransactionAttributeSourceAdvisor(事务的Advisor),并在Advisor中注入其所依赖的被实例化(注意实例化并不意味着创建成功)的事务属性和事务advice,至此,虽然我们没有通过XML或@Aspect显式定义事务的切面逻辑,但通过自定义的命名空间处理器,仍然隐式注册到了Spring容器中。

找到需要进行增强(被代理)的类,并为生成target类的代理提供相应的信息

上一步我们向Spring容器中注册了切面逻辑的Spring容器内部类,接着我们需要将该切面逻辑增强到对应的类中,而Spring容器在Bean的生命周期中通过BeanPostProcessor对外提供了几个Bean的增强点,所有的Bean在进行初始化时,都会调用实现了BeanPostProcessor接口的类的postProcessAfterInitialization()方法;而切面注入的BeanPostProcessor实现类InfrastructureAdvisorAutoProxyCreator实现了InstantiationAwareBeanPostProcessor接口,实现这个接口会使得在实例化Bean完成后(即调用完postProcessAfterInitialization()方法后)返回其增加了切面逻辑的代理对象,之后所有对target对象的get都会返回对应的代理对象, 而在postProcessAfterInitialization()这个方法中,我们会获取BeanFactory中的所有Advisor.class的实现类,然后对于每个Bean找出其匹配的List<advisor>,并通过反射获取@Transactional中的注解信息用以实例化TransactionAttributeSource对象,匹配方法又分为引介增强普通(pointCut)增强,对于使用比较多的PonitCut增强,会通过ClassFilter()MethodMatcher()方法匹配当前Bean是否符合当前的切点,符合则返回代理对象,否之则返回原本对象。

总结

至此,我们分析完成了@Transactonal注解在Spring中是如何生效的;最关键的是对于命名空间的解析,向Spring容器中注册相应的切面advsior,并在每个Bean的实例化完成后,通过获取其匹配的切面用以增强原有Bean,并返回其代理。