为什么需要三级缓存

候选人小刘在面试拼多多 P7 时,面试官追问他:

"Spring 用三级缓存解决循环依赖,我问你一个问题:为什么是三级,不是两级?"

小刘说:"因为需要提前暴露 Bean..."

面试官:"那你说说两级够不够?为什么?"

小刘想了半天,说:"好像不太够..."

面试官继续追问:"那多出来的那一级,具体解决了什么问题?"

小刘彻底哑火了。

【面试官心理】 这道题是 P7 的筛选题。三级缓存的名字很多人都知道,但能说清楚"为什么需要三级"而不是"两级就够"的人,才是真正理解 Spring 设计哲学的候选人。关键在于:两级缓存无法同时解决循环依赖和 AOP 代理的问题。


一、核心问题 🔴

1.1 问题拆解

第一层:基本认知

  • "Spring 的三级缓存分别叫什么名字?"
  • "每级缓存的作用是什么?"

第二层:设计哲学

  • "为什么需要三级?两级不够吗?"
  • "如果只用两级缓存,会有什么问题?"

第三层:AOP 关联

  • "三级缓存和 AOP 代理有什么关系?"
  • "早期暴露的 Bean 引用,是原始对象还是代理对象?"

第四层:源码验证

  • "为什么 singletonFactories 存的是 ObjectFactory 而不是直接存对象?"
  • "BeanPostProcessor 在三级缓存中扮演什么角色?"

1.2 ❌ 错误示范

候选人原话 A:"三级缓存是一级、二级、三级,这样设计是为了分层管理。"

问题诊断

  • "分层管理" 这种回答太笼统,没有具体内容
  • 说明完全不理解每级缓存的具体职责
  • 没有理解为什么需要这个分层

候选人原话 B:"两级够用的,不需要三级缓存。"

问题诊断

  • 这是完全错误的理解
  • 说明没有仔细想过两级缓存会出什么问题
  • 完全没有理解 AOP 代理和循环依赖的组合问题

候选人原话 C:"第三级缓存是为了处理 AOP 代理,普通 Bean 不需要。"

问题诊断

  • 只理解了 AOP 部分,没理解完整的逻辑链
  • 不知道 ObjectFactory 的真正作用
  • 把两个问题割裂开了

1.3 标准回答

P5 回答:三级缓存各司其职

Spring 的三级缓存在 DefaultSingletonBeanRegistry 中定义:

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry {

    // 一级缓存:完整的单例 Bean(已完全初始化)
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    // 二级缓存:早期的单例 Bean(已完成实例化,未完成属性填充和初始化)
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

    // 三级缓存:单例工厂(用于创建早期引用,支持 AOP 代理)
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}

职责划分:

  • 一级缓存:存放完全初始化好的 Bean,getBean 时直接返回这里
  • 二级缓存:存放提前暴露的 Bean 引用,用于打破循环依赖
  • 三级缓存:存放 ObjectFactory,用于延迟创建早期引用(支持 AOP)

1.4 追问升级

追问 1:两级缓存会出什么问题?

假设我们只有一级和二级缓存(去掉三级),看看会发生什么:

场景:A 有切面(需要 AOP 代理),A → B → A 循环依赖

【错误的两级缓存流程】:
T1: 创建 A
    ├─ 实例化 A(原始对象)
    ├─ 存入一级缓存?—— ❌ 不行,一级缓存只存完全初始化的 Bean
    ├─ 存入二级缓存
    └─ 填充属性时发现需要 B

T2: 创建 B
    ├─ 实例化 B
    ├─ 存入二级缓存
    └─ 填充属性时发现需要 A

T3: B 获取 A 的早期引用
    ├─ 从一级缓存获取 —— 没有(A 还没完全初始化)
    ├─ 从二级缓存获取 —— 有!返回 A 的原始对象
    └─ 问题:A 是原始对象,不是 AOP 代理!
        ├─ B 拿到的 A 不是代理
        ├─ B 调用 A 的方法不会走切面
        └─ 如果 A 有 @Transactional、@Async 等,全部失效!

所以,两级缓存的问题在于:无法在循环依赖时正确处理 AOP 代理

核心原因:Spring 需要在"提前暴露引用"时判断是否需要创建代理,但这个判断需要 BeanPostProcessor 的参与。如果直接把实例存入二级缓存,就失去了这个判断机会。

追问 2:ObjectFactory 的真正作用

这就是三级缓存的设计精髓:

// 三级缓存存的不是 Bean 实例,而是 ObjectFactory
public interface ObjectFactory<T> {
    T getObject() throws BeansException;
}

// 当调用 getObject() 时,才会真正创建早期引用
singletonFactories.put(beanName, () -> getEarlyBeanReference(beanName, mbd, beanInstance));

getEarlyBeanReference 的逻辑:

// AbstractAutowireCapableBeanFactory.java
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;

    // 【关键】调用所有 SmartInstantiationAwareBeanPostProcessor
    // AspectJAwareAdvisorAutoProxyCreator 就在这里决定是否创建代理
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp =
                    (SmartInstantiationAwareBeanPostProcessor) bp;
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}
【三级缓存的精确流程】:
T1: A 开始创建
    ├─ 实例化 A(原始对象 a)
    ├─ 存入三级缓存:singletonFactories.put("a", ObjectFactory→a)
    │   └─ 这个 ObjectFactory 记住了原始对象 a
    └─ 填充属性时需要 B

T2: B 开始创建
    └─ 填充属性时需要 A 的早期引用
        ├─ 检测到 "a" 正在创建
        ├─ 从三级缓存取出 ObjectFactory
        ├─ 调用 ObjectFactory.getObject()
        │   └─ getEarlyBeanReference()
        │       └─ AspectJAwareAdvisorAutoProxyCreator 检查是否需要代理
        │           └─ 返回 a 的代理(如果有切面)或原始对象(如果没有切面)
        ├─ 返回代理对象 pA
        ├─ 存入二级缓存:earlySingletonObjects.put("a", pA)
        └─ 删除三级缓存:singletonFactories.remove("a")

T3: B 完成,返回给 A
    └─ A 拿到 B 的引用

T4: A 完成初始化
    └─ 存入一级缓存:singletonObjects.put("a", A的最终对象)
    └─ 删除二级缓存:earlySingletonObjects.remove("a")
💡

关键洞察:三级缓存的作用是延迟创建早期引用,并且在创建早期引用时能够正确处理 AOP 代理。没有三级缓存,就无法在循环依赖时判断 Bean 是否需要代理。

追问 3:为什么存入二级缓存后要删除三级缓存?

这是一个容易被忽略的细节:

// DefaultSingletonBeanRegistry.getSingleton(String, ObjectFactory<?>)
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject != null) {
            return singletonObject;
        }

        // 【创建 Bean】
        singletonObject = singletonFactory.getObject();

        // 【关键】存入一级缓存后,清理二三级
        addSingleton(beanName, singletonObject);
        return singletonObject;
    }
}

protected void addSingleton(String beanName, Object singletonObject) {
    synchronized (this.singletonObjects) {
        // 存入一级缓存
        this.singletonObjects.put(beanName, singletonObject);
        // 【删除三级缓存】
        this.singletonFactories.remove(beanName);
        // 【删除二级缓存】
        this.earlySingletonObjects.remove(beanName);
    }
}

原因:

  1. 防止重复创建:如果 ObjectFactory 还在,后续其他 Bean 获取早期引用时会重复调用
  2. 保证单一实例:一个 Bean 只能有一个早期引用
  3. 释放内存:ObjectFactory 完成任务后可以清理

追问 4:BeanPostProcessor 在三级缓存中的角色

没有 BeanPostProcessor,就没有 AOP 代理判断,三级缓存的意义就大打折扣:

// Spring 内部注册了这些与循环依赖相关的 BeanPostProcessor

// 1. AspectJAwareAdvisorAutoProxyCreator(创建 AOP 代理)
//    └─ 实现了 SmartInstantiationAwareBeanPostProcessor
//    └─ getEarlyBeanReference() 方法决定是否创建代理

// 2. AutowiredAnnotationBeanPostProcessor(处理 @Autowired)
//    └─ 在 postProcessProperties() 中完成注入
//    └─ 注入时会触发 getBean,从而触发循环依赖解析

// 3. AnnotationAwareAspectJAutoProxyCreator(处理 @AspectJ)
//    └─ 同 AspectJAwareAdvisorAutoProxyCreator

如果没有 SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference() 方法,Spring 就无法在早期引用阶段创建 AOP 代理,所有被代理的 Bean 在循环依赖场景下都会退化成原始对象。

二、延伸问题 🟡

2.1 如果 Bean 不需要 AOP,需要三级缓存吗?

即使 Bean 不需要 AOP,三级缓存仍然有其价值:

【场景】:A → B → A,无 AOP

T1: A 开始创建,存入三级缓存
T2: B 开始创建
T3: B 获取 A 的早期引用
    ├─ 从三级缓存取 ObjectFactory
    ├─ 调用 getEarlyBeanReference()
    │   └─ 没有 BeanPostProcessor 要处理,直接返回原始对象
    ├─ 存入二级缓存
    └─ 删除三级缓存
T4: A 完成,存入一级缓存

如果没有三级缓存,两级缓存也能工作,但:

  1. 需要在实例化时就决定是否存入二级缓存
  2. 无法延迟到真正需要时才创建早期引用
  3. 代码逻辑会更加复杂
💡

实际上,Spring 源码中确实有一个优化:如果检测到没有 SmartInstantiationAwareBeanPostProcessor,会跳过三级缓存直接用二级缓存。但这属于实现细节,面试中不需要主动提及。

2.2 三级缓存和 Spring Cloud Refresh 的冲突

Spring Cloud 的配置刷新机制(@RefreshScope)会和三级缓存产生有趣的互动:

@RefreshScope
@Configuration
public class MyConfig {
    @Bean
    public MyBean myBean() {
        return new MyBean();
    }
}

@RefreshScope 的原理:

  1. 每次 refresh 时,销毁旧 Bean,创建新 Bean
  2. 使用 GenericScope 替代普通单例作用域
  3. 旧 Bean 销毁时,需要从三级缓存中清除
// ContextRefresher.refresh()
public synchronized void refresh() {
    // 1. 刷新配置
    updateEnvironment();
    // 2. 重新加载 Bean
    refreshContext();
}

// refreshContext() 内部
// 会关闭旧的 ApplicationContext,创建新的
// 新的 ApplicationContext 有全新的三级缓存

这个机制在某些场景下(比如配置刷新时正好有 Bean 正在初始化)可能导致问题。

2.3 循环依赖检测时机

Spring 在什么时机检测循环依赖?

// AbstractBeanFactory.doGetBean()
protected <T> T doGetBean(String beanName, Class<T> requiredType, Object[] args, boolean typeCheckOnly) {
    Object sharedInstance = getSingleton(beanName);

    if (sharedInstance != null) {
        // 一级缓存命中
        return getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
    }

    // 【关键】检测循环依赖
    // 如果这个 Bean 正在创建中,说明有循环依赖
    if (isPrototypeCurrentlyInCreation(beanName)) {
        throw new BeanCurrentlyInCreationException(beanName);
    }

    // 如果是 singleton,检查是否正在创建
    if (mbd.isSingleton()) {
        // 这里会触发 getSingleton(String, ObjectFactory<?>)
        // 最终会调用 createBean
    }
}

检测的关键是 isPrototypeCurrentlyInCreation 和 singleton 创建时的标记:

// DefaultSingletonBeanRegistry
private final Set<String> singletonsCurrentlyInCreation =
    Collections.newSetFromMap(new ConcurrentHashMap<>(16));

// 在开始创建 Bean 时标记
protected void beforeSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.contains(beanName)
            && !this.singletonsCurrentlyInCreation.add(beanName)) {
        // 如果已经在集合中,说明有循环依赖!
        throw new BeanCurrentlyInCreationException(beanName);
    }
}

// 在创建完成后取消标记
protected void afterSingletonCreation(String beanName) {
    if (!this.inCreationCheckExclusions.remove(beanName)) {
        throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
    }
}

三、生产避坑

3.1 手动操作三级缓存的陷阱

绝对不要手动操作三级缓存,这会导致不可预料的问题:

@Service
public class BadService {
    @Autowired
    private DefaultListableBeanFactory beanFactory;

    public void badPractice() {
        // ❌ 错误做法:手动操作三级缓存
        beanFactory.getSingletonMutex(); // 锁住

        // 手动往三级缓存塞东西 —— 这会导致 Spring 状态不一致
        beanFactory.registerSingleton("myManualBean", new MyBean());

        // 问题:
        // 1. 绕过了 BeanPostProcessor
        // 2. Bean 没有经过正常生命周期
        // 3. AOP 代理不会被创建
        // 4. 依赖注入不会执行
        // 5. 循环依赖检测会失效
    }
}

正确做法:使用 BeanFactory.registerSingleton() 但同时手动触发生命周期:

@Service
public class GoodService {
    @Autowired
    private DefaultListableBeanFactory beanFactory;

    public void goodPractice() {
        // ✅ 正确做法:注册 BeanDefinition 而不是直接注册实例
        BeanDefinitionBuilder builder = BeanDefinitionBuilder
            .genericBeanDefinition(MyBean.class)
            .addPropertyReference("dependency", "otherBean");

        beanFactory.registerBeanDefinition("myBean", builder.getBeanDefinition());
        // Spring 会按正常流程创建这个 Bean
    }
}

3.2 三级缓存和事务的组合问题

循环依赖 + @Transactional 可能导致事务失效:

@Service
public class A {
    @Autowired
    private B b;

    @Transactional
    public void methodA() {
        b.methodB();
        // 其他数据库操作...
    }
}

@Service
public class B {
    @Autowired
    private A a;

    public void methodB() {
        // 调用 A 的另一个方法
        a.anotherMethod();
    }
}

如果 A 的早期引用(被 B 持有)不是代理对象,那么:

  • methodA() 的事务可能不生效(取决于 Spring 版本和具体实现)
  • @Async 也可能失效

Spring 内部通过 TransactionAwareInvocationHandler 等机制尽量处理这个问题,但最佳实践是尽量避免循环依赖

3.3 启动性能与三级缓存

三级缓存在每次循环依赖解析时都会调用 ObjectFactory.getObject(),如果 BeanPostProcessor 链很长,会有性能开销:

// 伪代码:每次获取早期引用都要遍历所有 BeanPostProcessor
public Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    for (BeanPostProcessor bp : getBeanPostProcessors()) { // N 次遍历
        if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
            exposedObject = ((SmartInstantiationAwareBeanPostProcessor) bp)
                .getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

如果 BeanPostProcessor 很多,循环依赖解析会成为启动瓶颈。

四、工程选型

4.1 是否需要了解三级缓存?

角色是否需要为什么
普通开发不需要日常使用不需要知道这些
框架开发者必须掌握开发框架扩展时必须理解
高级工程师建议了解排查复杂问题时有用
面试 P7必须掌握这是区分候选人的关键题

4.2 实际工作中的指导意义

理解了三级缓存的设计哲学,对实际工作有以下指导意义:

  1. 代码设计:尽量避免循环依赖,代码结构更清晰
  2. 问题排查:遇到循环依赖报错时,能快速定位问题
  3. 框架扩展:开发自定义 BeanPostProcessor 时,理解早期引用机制
  4. 性能优化:了解 BeanPostProcessor 对启动性能的影响

五、面试总结

为什么需要三级缓存?这道题考的不是"记住了三级缓存叫什么",而是"理解了问题的本质"。

两级缓存无法解决循环依赖 + AOP 代理的组合问题:

  • 两级缓存:无法在提前暴露引用时判断是否需要创建代理
  • 三级缓存:通过 ObjectFactory 延迟创建早期引用,在创建时通过 BeanPostProcessor 正确处理 AOP

核心洞察:

  1. 三级缓存存的是 ObjectFactory,不是 Bean——因为需要在获取早期引用时决定返回什么
  2. 第二级是为了避免重复创建早期引用——同一 Bean 只创建一次早期引用
  3. 第一级是最终结果——只有完全初始化好的 Bean 才会进入一级缓存

能回答到这个深度的候选人,基本都有 Spring 源码阅读经验。