为什么需要三级缓存
候选人小刘在面试拼多多 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);
}
}
原因:
- 防止重复创建:如果 ObjectFactory 还在,后续其他 Bean 获取早期引用时会重复调用
- 保证单一实例:一个 Bean 只能有一个早期引用
- 释放内存: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 完成,存入一级缓存
如果没有三级缓存,两级缓存也能工作,但:
- 需要在实例化时就决定是否存入二级缓存
- 无法延迟到真正需要时才创建早期引用
- 代码逻辑会更加复杂
💡
实际上,Spring 源码中确实有一个优化:如果检测到没有 SmartInstantiationAwareBeanPostProcessor,会跳过三级缓存直接用二级缓存。但这属于实现细节,面试中不需要主动提及。
2.2 三级缓存和 Spring Cloud Refresh 的冲突
Spring Cloud 的配置刷新机制(@RefreshScope)会和三级缓存产生有趣的互动:
@RefreshScope
@Configuration
public class MyConfig {
@Bean
public MyBean myBean() {
return new MyBean();
}
}
@RefreshScope 的原理:
- 每次 refresh 时,销毁旧 Bean,创建新 Bean
- 使用
GenericScope 替代普通单例作用域
- 旧 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 是否需要了解三级缓存?
4.2 实际工作中的指导意义
理解了三级缓存的设计哲学,对实际工作有以下指导意义:
- 代码设计:尽量避免循环依赖,代码结构更清晰
- 问题排查:遇到循环依赖报错时,能快速定位问题
- 框架扩展:开发自定义 BeanPostProcessor 时,理解早期引用机制
- 性能优化:了解 BeanPostProcessor 对启动性能的影响
五、面试总结
为什么需要三级缓存?这道题考的不是"记住了三级缓存叫什么",而是"理解了问题的本质"。
两级缓存无法解决循环依赖 + AOP 代理的组合问题:
- 两级缓存:无法在提前暴露引用时判断是否需要创建代理
- 三级缓存:通过 ObjectFactory 延迟创建早期引用,在创建时通过 BeanPostProcessor 正确处理 AOP
核心洞察:
- 三级缓存存的是 ObjectFactory,不是 Bean——因为需要在获取早期引用时决定返回什么
- 第二级是为了避免重复创建早期引用——同一 Bean 只创建一次早期引用
- 第一级是最终结果——只有完全初始化好的 Bean 才会进入一级缓存
能回答到这个深度的候选人,基本都有 Spring 源码阅读经验。