三级缓存(不是CPU的概念,而是一种技术上逻辑容错处理方案)
发布日期:2021-06-29 13:15:41 浏览次数:2 分类:技术文章

本文共 6866 字,大约阅读时间需要 22 分钟。

三级缓存(不是CPU的概念,而是一种技术上逻辑容错处理方案)

 

相信硬件出生的同学,对这个一眼就认为是CPU的三级缓存。

百科上解释的三级缓存

三级缓存是为读取二级缓存后未命中的数据设计的—种缓存,在拥有三级缓存的CPU中,只有约5%的数据需要从内存中调用,这进一步提高了CPU的效率。其运作原理在于使用较快速的储存装置保留一份从慢速储存装置中所读取数据且进行拷贝,当有需要再从较慢的储存体中读写数据时,缓存(cache)能够使得读写的动作先在快速的装置上完成,如此会使系统的响应较为快速。

它这个三级缓存是为了解决CPU高速缓冲与磁盘的差异化的媒介。

 

但是我今天想和大家分享的是另一种概念,当然他们都是被称之为“三级缓存”,但是我所想分享的是一种逻辑上的解决方案,和这个CPU三级缓存完全是两个概念。

 

就是 Spring 的循环依赖解决方案-->三级缓存

贴上官方文档地址

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#spring-core

 

我们复制一段话

 

翻译一下

循环依赖

如果主要使用构造函数注入,则可能会创建无法解决的循环依赖方案。
 
例如:A类通过构造函数注入需要B类的实例,而B类通过构造函数注入需要A类的实例。如果您将A类和B类的bean配置为相互注入,则Spring IoC容器会在运行时检测到此循环引用,并抛出 BeanCurrentlyInCreationException。
 
一种可能的解决方案是编辑某些类的源代码,这些类的源代码由设置者而不是构造函数来配置。或者,避免构造函数注入,而仅使用setter注入。换句话说,尽管不建议这样做,但是您可以使用setter注入配置循环依赖项。
 
与典型情况(没有循环依赖关系)不同,Bean A和Bean B之间的循环依赖关系迫使其中一个Bean在完全完全初始化之前被注入另一个Bean(经典的“鸡与蛋”场景)。

 

对于spring官方而言,早就发现了会存在循环依赖的问题,这种问题在我们初始化bean容器的过程中,就会出现,当A类初始化需要注入B类实例,而B类初始化需要注入类实例。那么这个时候就会出现相互依赖关系,就好像路口遇到两辆车,你不让我,我怎么过去,我怎么给你让路?那他又认为,对方不让我,我怎么过去,我怎么给对方让路?形成死循环了。

在spring而言,如果控制反转的容器出现了这个情况,就会直接抛出bean创建异常。

那么怎么解决呢?就是延迟类的属性注入,先实例化类,再后续注入所需属性。

那么这个解决方案,就需要我们尽量使用setter注入属性方式,而不是使用构造注入方式,因为如果构造注入没有实例对象,那么就无法实例化类,也就形成死循环。

官方将其认为和 先有鸡还是现有蛋的这个千古绕题比较,我觉得也是有道理的。

 

 

官方抛出了问题,那么我们看看具体源码中,是如何解决的。

 

1.通常,在spring启动的过程中,就会构造一个bean容器对象。那么顺藤摸瓜,我们从main方法,SpringApplication.run()方法中,看看他到底做了什么

 

2.进入到run方法中后,发现其是一个方法重载,我们在其下方就看到了这个重载方法,所以很多项目的启动spring的方法会有差别,但是最终调用的方法只会有一个。

 

3.进入到SpringApplication实例的run方法后,可以看到一个很长的方法代码块,

 

4.很多是spring启动的一些动作,我们简而言之吧,此文目的是为了了解spring循环依赖问题

我们看到上方有一个refreshContext(context);方法,进入,一层层进入,会直接发现一个refresh()的无参方法,直接进入该方法。可以看到做了很多事情,那么我们直接进入该方法中的 finishBeanFactoryInitialization(beanFactory);方法,见名知意,很清楚 “完成bean工厂的初始化”

 

5.进入到finishBeanFactoryInitialization方法中,可以看到最后一行代码

beanFactory.preInstantiateSingletons() 实例化单例对象

 

6.进入到preInstantiateSingletons()方法中,很长,一屏放不下,然后问我们可以很直观的发现,里面有两个for循环操作,并直接将该类中的 beanDefinitionNames 类成员变量进行循环,名字就叫bean名称集合。

 

两个循环到底做了什么操作呢?为什么需要循环两次?

哈哈,有注释,我们翻译一下:

 

//遍历一个副本以允许使用init方法,这些方法依次注册新的bean定义。

//虽然这可能不是常规工厂引导程序的一部分,但可以正常运行。
 
//触发所有非惰性单例bean的初始化...
 
//触发所有适用bean的初始化后回调...

是的,他是将类成员变量复制出一个局部变量的副本,然后对其进行注册bean容器中。

触发所有非惰性单例bean的初始化,什么是非惰性,什么又叫惰性单例bean呢?此处留个问题,后面我们会分析

触发所有bean的初始化后回调。

看完后,我们可以知道,我们先得从第一个循环入手。

 

先调用 getMergedLocalBeanDefinition(beanName);

获取一个 RootBeanDefinition的对象,叫bd,这是什么对象呢?

我们暂时可以将其任务是bean容器的bean对象一些配置吧,他可以获取到当前这个beanName中 id、别名、bean对象之间的引用关系等等;bean的注解;具体的工厂方法,工厂方法返回类型,工厂方法的Method对象;bean对象的构造函数,构造函数形参类型;bean的class对象等等信息。

从上图中,我们就可以看出他用到了这个bd的 isAbstract() isSingleton() isLazyInit()

所以我们就可以知道这个bd存了一些什么信息了。

从第一层判断,可以得出,抽象的、非单例的、懒初始化的都不会进入此方法。

 

然后我们看到判断当前beanName是否为工厂? isFacotryBean(beanName) ?

我们跳过,我们直接进入其 getBean(beanName)方法,直捣黄龙!

 

7.看到四个方法重载的 getBean()方法,多种获取bean对象的方法;但是这些都不重要,重要的是,他们都调用了同一个方法 doGetBean();真正执行获取bean对象的方法。

 

8.我们具体来看看doGetBean,到底是怎么获取的一个Bean对象的?(此处就不全部贴图,太长了)

我们可以看到其中有一个getSingleton(beanName)方法

 

9.进入getSingleton(String beanName)

又是方法重载(看来对于方法重载的使用,是一种很好的代码风格)

我们侧重看第二个方法的逻辑,注意此时的类名是“DefaultSingletonBeanRegistry”

这个时候,我们就需要注意其中的三个高频出现的类成员变量,我们点击其名字,看看定义他们的地方是如何描述的。

 

很简约的描述,都是缓存cache of;没错,这三个Map常量对象就是我们今天的主角,三级缓存

分别是一级缓存、三级缓存、二级缓存(从上至下),也可以叫做bean单例池、bean工厂缓存、早期bean单例池;我们大概混个眼熟,然后再看看他们到底分别怎么使用的。

 

10.回到刚刚的 getSingleton()方法中,我们可以看到

第一步,现从一级缓存中获取该beanName的实例。如果获取到了,那么将直接返回当前单例对象。如何没有获取到,并且当前bean实例正在并行创建中(如何判断是否正在并行创建中,看下图中的一个类成员集合,如果包含,则证明正在创建中),那么就进入同步器中 synchronized(this.singletonObjects),为何使用同步器呢?接下去看。

 

第二步,再去二级缓存中获取该beanName的实例。如果获取到了,那么立即返回当前对象。如果没有获取到,并且开启了早期单例池缓存,那么就进行更深层的获取。

第三步,再去三级缓存中获取到该beanName的对象构造工厂对象。判断是否为空,如果为空,那么将立即返回当前空对象。如果不为空,那么就开始执行构造对象方法singletonFactory.getObject()方法;生成了单例对象后,将该单例对象存放到二级缓存中,然后再将三级缓存中的构造工厂对象移除。(注意,此处就是为什么我们需要使用到同步器了,因为我们需要保证两个Map对象一个增加,一个删除是同步的。不然就会有安全问题。回到刚刚三个缓存的定义,我们还可以发现,三级缓存中,只有一级缓存是ConcurrentHashMap,其他两个都是HashMap,这里就是为什么这样定义的解释)

 

11.从上面的构造一个bean对象的逻辑中,如果一个bean对象是首次加载构造,那么他的BeanFactory对象在三级缓存中也是为空的,那么上述描述中也就会返回null,那么如果我们调用getSingleton()方法返回的是null,那么就会进入到下述逻辑中;(笔者断点可得)

 

那么逻辑将判断当前beanName是否创建中?是的话,则抛异常,因为我们在方法中已经判断过了如果是true,那么就有问题的。

接下来就是获取 getParentBeanFactory()方法,获取父bean工厂对象。很显然,我们此处获取的parentBeanFactory对象就是null的,然后进入下述图片中的判断,因为我们入参typeCheckOnly(仅限类型检查) == false,那么将直接进入markBeanAsCreated(beanName)标记beanName开始创建了。

 

12.开始创建对象了

 

获取bean对象的基础信息实体(上面讲过了),然后执行校验bean

看看是不是抽象类

 

再看看dependsOn是不是null(加载前依赖其他bean),此处跳过,我们不分析

 

然后他再判断mbd是不是单例的?

这才是重要的,他会调用getSingleton()方法,传入beanName,和一个lamda表达式(创建bean的表达式)他其中就是执行了createBean();

 

我们进入此getSingleton()方法,直接可以看到他内部调用了beanFactory对象的getObjects();

 

我们进入createBean()方法,一眼望去,就看见了doCreateBean()方法

 

进入 doCreateBean()方法

 

我们可以看到有个判断 this.allowCircularReferences 是否运行循环引用?

官方是默认支持循环引用的。

紧接着,我们可以看到这段代码

addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

添加单例工厂,一个lamda表达式,

 

继续分析 addSingletonFactory()方法,做了什么?

可以看到,使用了同步关键字,判断如果一级缓存中不包含当前beanName的话,将在三级缓存中添加该beanName的单例工厂,然后会将二级缓存中移除该beanName(原则上二级缓存中是不存在该beanName的),然后registeredSingletons也添加了该beanName,我们看到该map是一个有序注册集合,注释是:已注册的单例集,按注册顺序包含Bean名称。

 

完成了对象的实例化,那么接下来就是对对象的属性进行填充操作了

可以看到下图中方法populateBean(); 装豆

 

进入该populateBean()方法中,我们可以看到这个方法:applyPropertyValues()

添加属性值,那么我们继续进入该方法

 

进入该方法中可以看到,里面有个循环体,original

循环体代码块中有一个方法 resolveValueIfNecessary();

 

再进入该方法 resolveReference()

 

可以看到,最后又调用了this.beanFactory.getBean(resolvedName);方法

 

然后进入方法内部,发现,又是调用了doGetBean()方法了;

所以此处我们发现,当前创建一个bean对象的之后,会对这个对象的属性进行注入,然后又会去beanFactory()中调用获取bean对象,又是相同的流程。。

 

那么这个填充属性的流程中,又会去创建属性对象。然后通过缓存解决无限递归方法问题。也就是循环依赖问题。

 

13.总结上述流程基本上可以说 很难理解,因为我只是通过图片片段进行分析,建议各位同学还是去通过断点分析,多个对象键循环依赖是如何解决的。

本质来说,就是Spring的BeanFactory维护了3个Map对象,分别对应了我们所表达的三级缓存,然后3个缓存集合也行使着不同的责任,这里再贴张图。

/** Cache of singleton objects: bean name to bean instance. */private final Map
singletonObjects = new ConcurrentHashMap<>(256); /** Cache of singleton factories: bean name to ObjectFactory. */private final Map
> singletonFactories = new HashMap<>(16); /** Cache of early singleton objects: bean name to bean instance. */private final Map
earlySingletonObjects = new HashMap<>(16);

最终形成完整的bean对象都会存放到 singletonObjects中,作为完整的 单例池容器。其他两个缓存集合对象,可以认为是为了解决循环依赖的解耦,作为一个临时容器。

通常来说获取一个bean对象,分为以下4个步骤,

(1)获取单例对象 getSingleton()

(2)执行创建bean对象 doCreateBean()

(3)填充bean对象的属性(注入)populateBean()

(4)添加单例对象 addSingleton()

spring 通过三级缓存设计的解决方案,也可以称之为一种算法思想,例如 two sum。

个人认为spring官方能够通过3个缓存,提前暴露bean对象的方式,解决了我们业务中可能存在的问题,这是一种很好的容错解决方案。是值得我们去学习思考的,很多我们业务中也会存在类似的思想问题,我们如果能够学以致用,那么就学到位了。学习不是为了记住答案,而是为了能够了解他解决了什么问题。

 

14.最后其实也有一些其他的衍生问题,我们讨论一下;

(1)其实如果发现,类A和类B相互依赖,加以思考的话,我们会发现,其实通过一个缓存,我们也可以解决问题,那么为什么需要三级缓存呢?要知道,当我们构造出一个对象,如果这对象中的某个注入对象还未完成初始化,那么就不能投入到具体业务中去,因为很有可能抛出NPE异常。

(2)承接(1),那么二级缓存也可以解决两个对象相互依赖的NPE问题啊,一个用于存储单例池对象,一个用于存储早期的单例池。那么也是可以解决问题(1)中的问题的。但是我们需要考虑一个问题,如果三个对象之间,A依赖C,B也依赖于C,那么很有可能,A类中注入的C类和B类中注入的C类就不是同一个对象,很有可能是一个代理对象。那么我们引入一个bean工厂对象缓存,就可以解决这个问题了。我们通过工厂对象,可拿到完整的Bean对象C,并且A和B都是拿到一样的完整的Bean对象C的。

如果仅仅使用二级缓存,那么所有的Bean对象在初始化后,都需要创建一个代理对象,但是由于循环依赖的问题需要解决,我们就需要提前创建代理对象,那么对于其他Bean注入的属性,就是一个原始对象,那么就会产生问题,所以我们此处谨记,循环依赖有很多种情况,一级缓存可以解决一种问题,但是不能解决其他可能存在的问题,二级缓存也能解决一部分问题,但是有些问题,是违背了spring的设计原则的。

代理对象可以有很多种,但是我们必须要保证所有对象注入对象A的代理最后都必须是同一个对象。

 

 

转载地址:https://changemax.blog.csdn.net/article/details/113783861 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:explain 关键字分析(第一次发)【图片版】
下一篇:什么是“秒杀”?为什么传统项目中也有“秒杀”的概念?一起来分析一下.

发表评论

最新留言

表示我来过!
[***.240.166.169]2024年04月10日 03时24分10秒