面试官问:Spring事务实现原理?我懵逼了...

Posted Java资料站

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试官问:Spring事务实现原理?我懵逼了...相关的知识,希望对你有一定的参考价值。

优质文章,第一时间送达

  作者 |  insaneXs

来源 |  urlify.cn/RviaMj

前言

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

 /**
  * 是否有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<Object>() {
            @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。也有说这是为了r让CGLIB和JDK dynamic Proxy保持一致。
Emm...我觉得Duck不必。
不过Spring也没有把着属性限制死,如果你真想在non-public的方法上使用自动事务,使点手段修改这个变量即可(例如搞个高优先级的BeanPostProcessor,在通过反射修改这个变量)。但是尽量还是按照规范来吧。



粉丝福利:108本java从入门到大神精选电子书领取

以上是关于面试官问:Spring事务实现原理?我懵逼了...的主要内容,如果未能解决你的问题,请参考以下文章

字节一面,面试官拿 System.out.println() 考了我半个小时?我懵逼了...

字节一面,面试官拿 System.out.println() 考了我半个小时?我懵逼了...

面试官:为什么数据库连接池不采用 IO 多路复用?我懵逼了。。

面试官:为什么数据库连接池不采用 IO 多路复用?我懵逼了。。

阿里二面,面试官拿这个中间件问了我半个小时,我懵逼了……

阿里二面,面试官拿这个中间件问了我半个小时,我懵逼了……