什么是三级缓存?为什么需要?
作者:程序员马丁
在线博客:https://open8gu.com
大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。
面试话术
1. 什么是三级缓存?
当我们说起 Spring 的三级缓存,一般指的是 Spring 单例注册表 DefaultSingletonBeanRegistry
中的三个本地缓存:
- 一级缓存 singletonObjects:用于存放已经完成初始化的单例 Bean。
- 二级缓存 earlySingletonObjects:用于存放在某个 Bean 初始化过程中,由于循环依赖而在真正完成初始化之前就提前保留的早期 Bean。
- 三级缓存 singletonFactories:用于存放生成代理对象的临时代理工厂,当出现循环依赖的时候,如果有 Bean 需要生成代理,则会从此获取代理对象,并暴露到二级缓存中。
当创建 Bean 的时候,三级缓存分别在三个阶段被使用到:
- 当创建 Bean 实例后,Spring 会通过
addSingletonFactory
方法向三级缓存添加一个 ObjectFactory,而当调用它的 getObject 方法时,最终将会调用getEarlyBeanReference
方法创建一个代理对象。 - 当对 Bean 进行依赖注入和初始化时,如果存在循环依赖,那么这个阶段 Spring 会从三级缓存中获取 ObjectFactory 并创建代理对象,并在此后将获得的代理对象添加到二级缓存,然后将 ObjectFactory 从三级缓存删除。
- 若 Bean 存在循环依赖,那么当 Bean 完成初始化后,将会主动调用一次
getSingleton
从二级缓存中获取代理对象,然后返回给调用方,此后该 Bean 将会被重新注册到一级缓存中,此时 Spring 会主动清除二级和三级缓存。
2. 为什么需要三级缓存?
由于 Spring 的代理发生在 Bean 初始化完成后,当存在循环依赖时,由于进行依赖注入的 Bean 尚未初始化,因此被进行依赖注入的 Bean 实际上获得的是尚未代理的原始 Bean,此时对其来说被依赖 Bean 的 AOP 实际上是失效的。
为了解决这个问题,Spring 需要让尚未初始化的 Bean 在进行依赖注入时也能够被代理,因此需要尽可能早的为其指定代理方法,不过由于并不是所有的 Bean 都需要进行代理,并且生成代理的过程本身也会触发相关 Bean 的加载(比如 Advice 与 Advisor 等相关组件),所以这个真正创建代理对象的时机又要尽可能的延迟。
出于这种“提前占位,延迟代理”的原则,Spring 最终选择通过 ObjectFactory 来实现创建代理这个操作的延迟执行,并额外增加了第三级缓存来保存 ObjectFactory,通过让第二级缓存只允许通过第三级缓存获取数据的方式,保证了在当通过依赖注入获取一个尚未完成初始化的 Bean 时,也能正确的获取到被代理的 Bean。
问题详解
1. 什么是三级缓存?
当我们说起 Spring 的三级缓存,一般指的是 Spring 单例注册表 DefaultSingletonBeanRegistry
中的 singletonObjects、earlySingletonObjects 与 singletonFactories 三级缓存:
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
它们的作用如下:
- 一级缓存 singletonObjects:用于存放已经完成初始化的单例 Bean。
- 二级缓存 earlySingletonObjects:用于存放在某个 Bean 初始化过程中,由于循环依赖而在真正完成初始化之前就提前保留的早期 Bean。
- 三级缓存 singletonFactories:用于存放生成代理对象的临时代理工厂,当出现循环依赖的时候,如果有 Bean 需要生成代理,则会从此获取代理对象,并暴露到二级缓存中。
由于
AbstractBeanFactory
继承了DefaultSingletonBeanRegistry
,因此我们基本可以认为所有的BeanFactory
都是基于它扩展的,具体内容可参见:✅ 什么是 BeanFactory?
2. 三级缓存是如何运行的?
2.1. 二级缓存数据的来源
首先,我们关注 DefaultSingletonBeanRegistry
的 getSingleton
方法,这个方法向我们展示了数据是如何从第三级缓存转移到第二级缓存:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 尝试从一级缓存获取已经初始化的 Bean
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 如果 Bean 尚未初始化,且正在创建中,则尝试基于双重检查从二级缓存获取它
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// 再检查一遍一级缓存,确定拿到锁时其他线程是不是已经完成的Bean的初始化
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
// 确定二级缓存没有,尝试从三级缓存获取 ObjectFactory
// 如果 ObjectFactory 存在,那么创建一个 Bean 并将其放到二级缓存,然后将其从三级缓存移除
// 调用 ObjectFactory.getObject 实际上就会创建一个代理对象
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
简单的来说,这个方法做了这么一件事情:
- 先检查一级缓存,如果有就返回;
- 再检查二级缓存,如果有就返回,如果没有,那么就加锁准备进行初始化;
- 加锁后,检查三级缓存,如果还没有,就返回空,但是如果有 ObjectFactory ,那么就将其取出,调用 getObject 方法获取 Bean,然后再将其添加到二级缓存后,再把 ObjectFactory 从三级缓存移除。
也就是说,二级缓存的数据实际上是从三级缓存转移过来的。
那么,三级缓存的数据又是从何而来?
2.2. 三级缓存的数据来源
这个问题就要追溯到 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
方法了,当我们从 Spring 容器获取一个 Bean 时,最终基本都会走到这个方法上。
这里我们摘出关键的一部分代码:
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {
// ... ...
// 如果允许循环依赖,那么就为其在三级缓存添加一个 ObjectFactory
// 当后续调用 ObjectFacetry.getObject() 方法时,
// 将会直接调用 getEarlyBeanReference(beanName, mbd, bean) 方法
// 该方法实际上会调用 AbstractAutoProxyCreator 去尝试创建一个代理对象
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isTraceEnabled()) {
logger.trace("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// 对 Bena 进行依赖注入和初始化
// 注意,如果在进行依赖注入的过程中,会通过上述的 getSingleton 方法获取 Bean
// 而如果存在循环依赖,那么此时就会触发 ObjectFacetry.getObject,
// 进而通过 getEarlyBeanReference(beanName, mbd, bean) 方法直接创建代理对象
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}
// 当存在依赖注入的时候,这里会再次通过 getSingleton 获取 Bean
// 此时由于在 populateBean 这一步已经触发了代理对象的创建,
// 因此这里获取到的实际上是二级缓存中的代理对象,
// 这里将 earlySingletonReference 赋值给了 exposedObject,
// 确保最终整个方法执行完毕后返回的就是那个代理对象
// 等到调用方将获得的代理对象添加到一级缓存时,二级缓存就会被移除
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}
// ... ...
return exposedObject;
}
//
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
// SmartInstantiationAwareBeanPostProcessor 的实现类有不少
// 但是实现了 getEarlyBeanReference 方法的只有 AbstractAutoProxyCreator
// 因此这个方法某种程度上来说就是专门为代理开的口子
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}
简而言之,这个方法做的事情基本可以分为三个阶段:
- 第一个阶段,当创建 Bean 实例后,Spring 会通过
addSingletonFactory
方法向三级缓存添加一个 ObjectFactory,而当调用它的 getObject 方法时,最终将会调用getEarlyBeanReference
方法创建一个代理对象。 - 第二个阶段,对 Bean 进行依赖注入和初始化,如果存在循环依赖,那么这个阶段会因为调用了
getSingleton
从而导致数据从三级缓存加载到二级缓存。 - 第三个阶段,若 Bean 存在循环依赖,那么当 Bean 完成初始化后,将会主动调用一次
getSingleton
从二级缓存中获取代理对象,然后返回给调用方。这对象会在下文提到的步骤中被添加到一级缓存。
这里有一个有意思的地方,此处使用的 SmartInstantiationAwareBeanPostProcessor 有不少实现类,不过实现了 getEarlyBeanReference 方法的只有 AbstractAutoProxyCreator,因此,从某种程度上来说,这个方法就是 Spring 专门为代理留的后门。
2.3. 一级缓存数据的来源
最后,我们关注一级缓存的数据来源,实际上,当 BeanFactory 正常的创建完一个 Bean 以后,最终都会间接的通过 DefaultSingletonBeanRegistry#�addSingleton
方法添加到一级缓存中:
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
�这个过程也很简单,就是直接将 Bean 从二级和三级缓存中移除(如果有的话),然后再将其注册到一级缓存中。
整个过程实际上都属于 Bean 生命周期的一部分,更多的细节请参见:✅ Bean 的生命周期?
3. 为什么需要三级缓存?
3.1. 需要几级缓存才能解决循环依赖?
从理论上来说,循环依赖问题只需要两级缓存就可以解决。
其中,第一级缓存用于存放已经初始化好的对象,而第二级缓存用于存放当前正在创建的对象。
当一个对象创建完毕以后就直接暴露到二级缓存,然后进行依赖注入:
- 如果不存在循环依赖,直接尝试从第一级缓存获取初始化好的对象,如果没有就正常的走创建流程。
- 如果存在循环依赖,由于整个链路上已经创建好的、等到初始化的对象都已经暴露在第二级缓存了,此时直接从第二级缓存取就行。
当完成 Bean 的初始化后,就再讲 Bean 从二级缓存移除,然后转移到一级缓存里面。
3.2. 二级缓存与 SpringAOP 代理的时机问题
总所周知,一般情况下,SpringAOP 会通过 AbstractAutoProxyCreator
在后处理阶段为目标 Bean 创建代理对象,也就说,在正常情况下 Spring 需要等到一个 Bean 初始化完毕才能创建代理。
但是,如果存在循环依赖时,就会面临等 Spring 生成代理对象了但是原始的 Bean 已经被注入到了其他 Bean 的情况,这等于代理了个寂寞。
举个例子,我们假设现在存在一个 BeanA -> BeanB -> BeanA 的循环依赖关系,当 BeanB 创建完毕后,它直接通过依赖注入获得了原始的 BeanA,此后 Spring 就算再为 BeanA 生成代理对象也与 BeanB 无关了,它手中的 BeanA 始终是未被代理的原始对象。
总而言之,如果要保证即使是循环依赖,Bean 也可以正确的拿到被代理的 Bean,那么依据现有的代理机制,只靠单纯的两级缓存是无法解决问题的。
3.3. 第三级缓存是如何解决这个问题的?
�面对这种尴尬的情况,我们第一时间可以想到两种解决方案:
- 当 BeanB 使用了 BeanA 后,记录引用关系,等到后面的 BeanA 完成初始化了,再回过头来重新进行一次依赖注入,或者想想其他办法替换 BeanA。
- 当 BeanB 引用 BeanA 的时候,立刻对 BeanA 生成代理对象。
而第一个解决方案显然不可行,因为我们无法保证 BeanB 在拿到了 BeanA 后,是否会在重新注入代理对象前就拿着 BeanA 进行一些操作,这种方案会带来无法接受的风险。
但是第二个方案也有问题,不加以区分就对所有存在循环依赖的 Bean 进行代理也无法让人接受,即使在这种情况下,我们也需要保证代理工厂可以正确的评估 Bean 是否需要代理。并且,Spring 本身的 Bean 加载机制决定了在代理工厂运行的过程中,一定会导致更多的组件(比如 Advisor 或者相关的 Bean)触发加载,为了安全起见,这个过程肯定越晚触发越好。
如此以来,既然第一种方案无法接受,那就只能采用第二种方案,而对于第二种方案存在的问题,我们在实施的时候需要遵循两个原则来尽可能规避它们:
- 提前占位:所有可能存在循环依赖的 Bean 都存在需要提前代理的可能性,需要尽可能早的标记它们,并为其指定好创建代理的方式。
- 延迟代理:代理工厂判断 Bean 是否需要代理并且生成代理的过程中,会触发其他 Bean 的加载,因此这一步需要尽可能晚的进行。
到这里,我们其实已经基本了解了 Spring 的思路了:
- 为了尽可能的延迟真正创建代理类的时机,Spring 选择通过 ObjectFactry 来实现延迟调用。
- 为了尽可能早的标记可能需要提前代理的 Bean,需要一个独立的缓存来存放用于替代 Bean 的 ObjectFactory,并确保第二级缓存初始化的时候总是通过该缓存获取数据。
总的来说,Spring 通过第三级缓存 + ObjectFactory 的方式优雅的解决了循环依赖中的代理问题。
- 需要注意的是,这种方式实际上依然打破了 Spring 原本“在 Bean 初始化后才进行代理”的规则。
- 对于一些特殊的 Bean —— 比如被标记为懒加载的 Bean,或者 SpringWeb 中的 Request 对象 —— 它们不需要经过这个过程,而是默认就会进行代理,只不过不走代理工厂,具体可以参见:✅ Spring 的懒加载和延迟注入是什么?两者有何区别?
3.4. 有其他替代方案吗?
从理论上来说,第三级缓存 + ObjectFactory 只是 “提前占位,延迟代理” 这个思路下众多解决方案中的比较合适的一种,如果面试官一定要追问这个问题,那么基于这个思路,我们显然也可以提供一些别的方案。
比如,我们可以考虑下,如果直接放弃第三级缓存,利用现有的第二级缓存:
- 添加 ObjectFactory 的时候,直接添加到第二级缓存,而不是第三级缓存;
- 当从
DefaultSingletonBeanRegistry
中获取 Bean 的时候,如果一级缓存没有,尝试去二级缓存里面获取数据时,需要额外做一个判断:- 如果是 ObjectFactory,那就将其替换为通过 ObjectFactory.getObject() 方法获得的代理对象,然后返回这个代理对象;
- 如果不是 ObjectFactory,那就直接返回获取到的对象。
这里我们给出修改后的 getSingleton
示例代码,你可以理解一下:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 尝试从一级缓存获取已经初始化的 Bean
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 如果 Bean 尚未初始化,且正在创建中,则尝试基于双重检查从二级缓存获取它
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
// 再检查一遍一级缓存,确定拿到锁时其他线程是不是已经完成的Bean的初始化
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 从第二级缓存获取缓存的 Bean
// 如果是 ObjectFactory,则将其替换为 singletonObject
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject instanceof ObjectFactory) {
singletonObject = ((ObjectFactory<?>)singletonObject).getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
}
}
}
}
}
return singletonObject;
}
当然,这个公用第二级缓存的方案显然不如使用独立的第三级缓存优雅,并且考虑到 Spring 容器中可能确实存在类型为 ObjectFactory 的 Bean,因此添加工厂的时候可能需要使用一个不对用户保留的内部接口类来代替 ObjectFactory。