面试官:谈谈Spring事务实现原理

业余草

共 10790字,需浏览 22分钟

 ·

2021-10-24 14:12

你知道的越多,不知道的就越多,业余的像一棵小草!

你来,我们一起精进!你不来,我和你的竞争对手一起精进!

编辑:业余草

cnblogs.com/insaneXs/p/13638034.html

推荐:https://www.xttblog.com/?p=5281

前言

对于一个应用而言,事务的使用基本是不可避免的。虽然 Spring 给我们提供了开箱即用的事务功能 @Transactional,但是,自带的事务功能却也存在控制粒度不够的缺点。更糟糕的是,@Transactional在某些情况下就失效了。可能一些读者 baidu/google 一下解决办法后,失效的问题确实解决了。但是由于不了解底层的原理,这样的问题可能在今后的工作中往复出现。

Spring事务

本文就为大家揭开@Transactional下的秘密。

原生的事务管理

在没有 Spring 存在的时候,事务就已经诞生了。其实框架依赖的还是底层提供的能力,只不过它对这一过程的抽象和复用。

这里我们用底层的 API 来了解下事务管理的过程( JDBC 为例):

// 获取mysql数据库连接
Connection conn = DriverManager.getConnection("xxxx");
conn.setAutoCommit(false);
statement = conn.createStatement();
// 执行sql,返回结果集
resultSet = statement.executeQuery("xxxx");
conn.commit(); //提交
//回滚
//conn.rollback();

上面是一个原生操作事务的一个例子,这些过程也是 Spring 事务逃不开的,只不过在为了编程的效率让这一过程自动化或是透明化的你无法感知罢了。
而我们之后做的就是逐步还原这一自动化的过程。

Spring提供的事务API

Spring 提供了很多关于事务的 API。但是最为基本的就是PlatformTransactionManagerTransactionDefintionTransactionStatus

事务管理器 PlatformTransactionManager

PlatformTransactionManager是事务管理器的顶层接口。事务的管理是受限于具体的数据源的(例如,JDBC 对应的事务管理器就是DatasourceTransactionManager),因此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;
}

同时为了简化事务管理器的实现,Spring 提供了一个抽象类AbstractPlatformTransactionManager,规定了事务管理器的基本框架,仅将依赖于具体平台的特性作为抽象方法留给子类实现。

事务状态 TransactionStatus

事务状态是我对TransactionStatus这个类的直译。其实我觉得这个类可以直接当作事务的超集来看(包含了事务对象,并且存储了事务的状态)。PlatformTransactionManager.getTransaction()时创建的也正是这个对象。
这个对象的方法都和事务状态相关:

public interface TransactionStatus extends TransactionExecutionSavepointManagerFlushable {
    /**
     * 是否有Savepoint Savepoint是当事务回滚时需要恢复的状态
     */

    boolean hasSavepoint();

    /**
     * flush()操作和底层数据源有关,并非强制所有数据源都要支持
     */

    @Override
    void flush();

}

此外,TransactionStatus还从父接口中继承了其他方法,都归总在下方:

/**
* 是否是新事务(或是其他事务的一部分)
*/

boolean isNewTransaction();

/**
* 设置rollback-only 表示之后需要回滚
*/

void setRollbackOnly();

/**
* 是否rollback-only
*/

boolean isRollbackOnly();

/**
* 判断该事务已经完成
*/

boolean isCompleted();


/**
* 创建一个Savepoint
*/

Object createSavepoint() throws TransactionException;

/**
* 回滚到指定Savepoint
*/

void rollbackToSavepoint(Object savepoint) throws TransactionException;

/**
* 释放Savepoint 当事务完成后,事务管理器基本上自动释放该事务所有的savepoint
*/

void releaseSavepoint(Object savepoint) throws TransactionException;

事务属性的定义 TransactionDefinition

TransactionDefinition表示一个事务的定义,将根据它规定的特性去开启事务。

事务的传播等级和隔离级别的常量同样定义在这个接口中。

/**
 * 返回事务的传播级别
 */

default int getPropagationBehavior() {
     return PROPAGATION_REQUIRED;
}

/**
 * 返回事务的隔离级别
 */

default int getIsolationLevel() {
     return ISOLATION_DEFAULT;
}

/**
 * 事务超时时间
 */

default int getTimeout() {
     return TIMEOUT_DEFAULT;
}

/**
 * 是否为只读事务(只读事务在处理上能有一些优化)
 */

default boolean isReadOnly() {
     return false;
}

/**
 * 返回事务的名称
 */

@Nullable
default String getName() {
     return null;
}

/**
 * 默认的事务配置
 */

static TransactionDefinition withDefaults() {
     return StaticTransactionDefinition.INSTANCE;
}

编程式使用Spring事务

有了上述这些 API,就已经可以通过编程的方式实现 Spring 的事务控制了。
但是 Spring 官方建议不要直接使用PlatformTransactionManager这一偏低层的 API 来编程,而是使用TransactionTemplateTransactionCallback这两个偏向用户层的接口。
示例代码如下:

//设置事务的各种属性;可以猜测TransactionTemplate应该是实现了TransactionDefinition
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transactionTemplate.setTimeout(30000);

//执行事务 将业务逻辑封装在TransactionCallback中
transactionTemplate.execute(new TransactionCallback() {
    @Override
    public Object doInTransaction(TransactionStatus transactionStatus) {
            //....   业务代码
    }
});

以上就是 Spring 事务最基本的原理。但是为什么这些过程对我们似乎都不可见呢?那是因为这些过程都「通过AOP的方式被织入」了我们的业务逻辑中。
所以,像要深入了解 Spring 事务原理,还需要了解 AOP 的原理。

AOP的原理

AOP 的实现机制有两种:Proxy-based 和 Weaving-based。

前者是依赖动态代理的方式达到对代理类增强的目的。后者应该是通过字节码增强的方式达到增强的目的。

在 Spring 中,一般默认使用前者。之后也仅是针对前者进行分析。

而 Spring 声明 AOP 的方式也有两种,一种是通过声明 Aspect,另一种是通过声明 Advisor。

无论是哪种方式,都需要表达清楚你要进行增强的逻辑 (what)和你要增强的地方(where)。即,需要告诉 Spring 你要增强什么逻辑,并且对哪些 Bean/哪些方法增强。

这里的 what 和 where 换成 AOP 中的概念分别就是对应AdvicePointcut

因为事务是通过 Advisor 声明 AOP 的,因此本文也只针对 Advisor 的实现展开分析。

动态代理

既然是动态代理,那么必然存在被代理类(Target),代理类(Proxy),以及类被代理的过程(因为对用户而言,并不知道类被代理了)。

被代理的类

被代理类是最容易知道的,就是那些被 Advisor 的 Pointcut 匹配(classFliter 匹配或是 methodMatches)到的类。

代理的类

而代理类是在运行时直接创建的。通常有两种方式:

  • JDK 的动态代理
  • CGLIB 的动态代理

二者的区别是 JDK 动态代理是通过实现接口的方式(代理的对象为接口),因此只能代理接口中的方法。

而 CGLIB 动态代理是通过继承的方式,因此可以对对象中的方法进行代理,但是由于是继承关系,无法代理 final 的类和方法(无法继承),或是 private 的方法(对子类不可见)。

创建代理及取代目标类的过程

创建代理及取代目标类主要是应用了 Spring 容器在获取 Bean 时留下的一个拓展点。

Spring 在getBean的时候,如果 Bean 还不存在会分三步去创建 Bean:

  • 实例化
  • 填充属性
  • 初始化

实例化通常是通过反射创建 Bean 对象的实例,此时得到的 Bean 还只是一个空白对象。

填充属性主要是为这个 Bean 注入其他的 Bean,实现自动装配。
而初始化则是让用户可以控制 Bean 的创建过程。

为 Bean 创建代理,并取代原有的 Bean 就是发生在初始化这一步,更具体的是在BeanPostProcessor.postProcessorAfterInitialization()中。

动态代理也有可能在实例化之前直接创建代理,这种情况发生在InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()中,此时的实例化过程不再是我们上文介绍的通过简单反射创建对象。

在众多的BeanPostProcessor中有一类后置处理器就是专门用于创建代理的。例如,我们要介绍的AbstractAdvisorAutoProxyCreator
看一下AbstractAutoProxyCreator创建代理的流程:

  1. 先确认是否已经创建过代理对象(earlyProxyReferences,避免对代理对象在进行代理)
  2. 如果没有,则考虑是否需要进行代理(通过wrapIfNecessary)
  3. 如果是特殊的Bean 或者之前判断过不用创建代理的Bean则不创建代理
  4. 否则看是否有匹配的Advise(匹配方式就是上文介绍的通过PointCut或者IntroducationAdvisor可以直接匹配类)
  5. 如果找到了Advisor,说明需要创建代理,进入createProxy
  6. 首先会创建ProxyFactory,这个工厂是用来创建AopProxy的,而AopProxy才是用来创建代理对象的。因为底层代理方式有两种(JDK动态代理和CGLIB,对应到AopProxy的实现就是JdkDynamicAopProxyObjenesisCglibAopProxy),所以这里使用了一个简单工厂的设计。ProxyFactory会设置此次代理的属性,然后根据这些属性选择合适的代理方式,创建代理对象。
  7. 创建的对象会替换掉被代理对象(Target),被保存在BeanFactory.singletonObjects,因此当有其他Bean希望注入Target时,其实已经被注入了Proxy。

以上就是 Spring 实现动态代理的过程。

Spring注解式事务

我们从编程式事务了解了 Spring 事务 API 的基本使用方式,又了解了 Spring Advisor 的原理。现在,我们在回到 Spring 注解式事务中,验证下注解式事务是否就是通过以上这些方式隐藏了具体的事务控制逻辑。

从@EnableTransactionManagement说起

@EnableTransactionManagement是开启注解式事务的事务。如果注解式事务真的有玄机,那么@EnableTransactionManagement就是我们揭开秘密的突破口。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement 
{

    /**
     * 用来表示默认使用JDK Dynamic Proxy还是CGLIB Proxy
     */

    boolean proxyTargetClass() default false;

    /**
     * 表示以Proxy-based方式实现AOP还是以Weaving-based方式实现AOP
     */

    AdviceMode mode() default AdviceMode.PROXY;

    /**
     * 顺序
     */

    int order() default Ordered.LOWEST_PRECEDENCE;

}

@EnableTransactionManagement注解看起来并没有特别之处,都是一些属性的配置。但它却通过@Import引入了另一个配置TransactionManagentConfigurationSelector

TransactionManangementConfigurationSelector

在 Spring 中,Selector通常都是用来选择一些 Bean,向容器注册BeanDefinition的(严格意义上 Selector 仅时选择过程,注册的具体过程是在ConfigurationClasspathPostProcessor解析时,调用ConfigurationClassParser触发)。
主要的逻辑就是根据代理模式,注册不同的 BeanDefinition。
对 Proxy 的模式而言,注入的有两个:

  • AutoProxyRegistrar
  • ProxyTransactionManagementConfiguration

AutoProxyRegistrar

Registrar 同样也是用来向容器注册 Bean 的,在 Proxy 的模式下,它会调用AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);向容器中注册InfrastructureAdvisorAutoProxyCreator。而这个类就是我们上文提到的AbstractAdvisorAutoProxyCreator的子类。
从而,我们完成了我们的第一个条件——AOP代理。

ProxyTransactionManagementConfiguration

ProxyTransactionManagementConfiguration是一个配置类,如果算上其继承的父类,一共是声明了四个类:

  1. TransactionalEventListenerFactory
  2. BeanFactoryTransactionAttributeSourceAdvisor
  3. TransactionAttributeSource
  4. TransactionInterceptor

后三个类相对比较重要,我们一一分析。

BeanFactoryTransactionAttributeSourceAdvisor

从名字看就知道这是一个 Advisor,那么它身上应该有 Pointcut 和 Advise。
其中的 Pointcut 是TransactionAttributeSourcePointcut,主要是一些 filter 和 matches 之类的方法,用来匹配被代理类。
而 Adivise 就是我们之后要介绍的TransactionInterceptor

TransactionAttributeSource

TransactionAttributeSource只是一个接口,扩展了TransactionDefinition,增加了isCandidateClass()的方法(可以用来帮助 Pointcut 匹配)。
这里使用的具体实现是AnnotationTransactionAttributeSource。因为注解式事务候选类(即要被代理的类)是通过@Transactional注解标识的,并且所有的事务属性也都来自@Transactional注解。

TransactionInterceptor

刚才我们说了,TransactionInterceptor就是我们找的Advise。
这个类稍微复杂一点,首先根据事务处理相关的逻辑都放在了其父类TransactionAspectSupport中。此外,为了适配动态代理的反射调用(两种代理方式),实现了MethodInterceptor接口。
也就是说,反射发起的入口是MethodInterceptor.invoke(),而反射逻辑在TransactionAspectSupport.invokeWithinTransaction()中。
我们可以简单看invokeWithTransaction()方法中的部分代码:

@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class targetClass,
        final InvocationCallback invocation)
 throws Throwable 
{

    
    TransactionAttributeSource tas = getTransactionAttributeSource();
    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);

    if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
        // 打开事务(内部就是getTransactionStatus的过程)
        TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

        Object retVal;
        try {
            // 执行业务逻辑 invocation.proceedWithInvocation();
        }
        catch (Throwable ex) {
            // 异常回滚
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        }
        finally {
            cleanupTransactionInfo(txInfo);
        }

        //省略部分代码
        
        //提交事物
        commitTransactionAfterReturning(txInfo);
        return retVal;
    }
}

虽然代码比我们之前的复杂,但是其主体结构依然是我们编程式事务的常见那几步。

行文至此,隐藏在 Spring 自动事务下的逻辑都分析的差不多了。未避免枯燥,本文并没有对代码一行行的分析,而是希望能够帮助读者把握大概的原理。

事务失效的常见情况及其背后的原因

数据库存储引擎不支持

常见的像 mySQL 的 myISAM 存储引擎就不支持事务功能。
这很好理解,说到底事务是数据库的功能,如果数据库本身就没有这个功能,那上层再怎么五花八门也是没用的。

未指定RollbackOn,且抛出的异常并非RuntimeException

这个背后的原因我们可以从DefualtTransactionAttribute中来找。

//可见默认触发回滚的异常是RuntimeException和Error
@Override
public boolean rollbackOn(Throwable ex) {
    return (ex instanceof RuntimeException || ex instanceof Error);
}

因此阿里巴巴代码规范倡议是显示指定 rollbackOn 为 Exception

同一个类中调用事务方法

这是在 Proxy 模式下才会失效的。
根据上文我们了解了 Spring 事务是机遇动态代理的,而当在类当中调用事务的方法时,动态代理是无法生效的,因为此时你拿到的 this 指向的已经是被代理类(Target),而非代理类(Proxy)。

非公开方法上的事务

如果你将@Transactional注解应用在一个 non-public 的方法上(即便是 protected 和 defualt 的方法),你会发现事务同样不生效(也是在 Proxy 模式下)。

有读者可能会疑问,GCLIB 的局限应该是在 private 或是 final 的方法上,private 方法代理失效还能理解,为什么 protected 和 defualt 方法也不行呢?

其实,non-public 方法失效的真正原因不是动态代理的限制,而是 Spring 有意为之。

之前我们介绍了TransactionAttributeSource会帮助 Pointcut 匹配类和方法,而在AnnotationTransactionAttributeSource中,有一个属性final boolean publicMethodsOnly表示是否只允许公有方法。这个属性在默认的构造函数中被设置了 true。因此代理只会对 public 方法生效。

网上找了下 Spring 这么设计的目的,有说业务类就是应该基于接口方法调用的,因此总为 public。也有说这是为了让 CGLIB 和 JDK dynamic Proxy 保持一致。

Emm... 我觉得 Duck 不必。

不过 Spring 也没有把着属性限制死,如果你真想在 non-public 的方法上使用自动事务,使点手段修改这个变量即可(例如搞个高优先级的BeanPostProcessor,在通过反射修改这个变量)。但是尽量还是按照规范来吧。

浏览 27
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报