Spring三级缓存硬核解密:二级缓存行不行?一级缓存差在哪?

导语: 上一篇我们深入探讨了Spring三级缓存如何解决循环依赖(点击回顾)。但灵魂拷问来了:为什么非得三级缓存?二级甚至一级缓存不行吗? 今天我们就撕开设计本质,结合Spring源码,彻底讲透三级缓存不可替代的深层原因!文末附高频面试题解析,助力斩获Offer!


一、再探三级缓存:设计目标与核心矛盾

Spring循环依赖解决方案的本质矛盾:

如何在Bean未完成初始化前暴露引用,同时确保该引用与最终Bean保持一致(尤其当存在AOP代理时)?

三级缓存的核心使命正是解决两大难题:

  1. 提前暴露引用:打破循环死锁
  2. 代理一致性 :确保依赖方注入的对象与最终Bean是同一个代理对象

二、简化方案对比:一级/二级缓存为何失败?

⚠️ 方案1:仅用一级缓存(singletonObjects

java 复制代码
// DefaultSingletonBeanRegistry.java
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

致命缺陷:无法隔离"半成品"Bean

  • 若将刚实例化(未初始化)的Bean直接放入一级缓存
  • 其他Bean可能获取到残缺对象(如:未注入依赖、未执行@PostConstruct
  • 结果:NPE或业务逻辑错误
java 复制代码
// 伪代码:错误的一级缓存方案
protected Object doCreateBean(...) {
    // 1. 实例化BeanA(半成品)
    Object bean = createBeanInstance(beanName, mbd, args);
    
    // ❌ 错误:将半成品直接放入一级缓存
    singletonObjects.put(beanName, bean);
    
    // 2. 填充属性(此时可能触发循环依赖)
    populateBean(beanName, mbd, instanceWrapper);
    // ... 初始化操作
}

结论 :一级缓存方案破产!必须区分成品Bean半成品Bean


⚠️ 方案2:仅用二级缓存(earlySingletonObjects + singletonObjects

java 复制代码
// DefaultSingletonBeanRegistry.java
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

无AOP时代:完美运行

以A→B→A循环为例:

  1. 实例化A → 原始对象存入earlySingletonObjects
  2. 填充A属性发现需要B → 实例化B
  3. 填充B属性需要A → 从earlySingletonObjects取原始A注入
  4. B初始化完成 → 移入singletonObjects
  5. A继续初始化 → 完成移入singletonObjects

引入AOP:灾难降临

java 复制代码
@Service
public class ServiceA {
    @Autowired private ServiceB b;
    @Async // 需要代理
    public void method() {...}
}

@Service
public class ServiceB {
    @Autowired private ServiceA a; // 注入的是原始对象!
}

致命问题

  1. B持有的A是原始对象,而非代理对象
  2. 最终容器中的A是代理对象
  3. 结果 :B调用的A.method()异步失效!且内存中存在两个A对象(原始对象+代理对象)

三、三级缓存的救赎:ObjectFactory动态决策

🔥 核心机制:延迟生成代理对象

三级缓存的关键

java 复制代码
// DefaultSingletonBeanRegistry.java
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

// 暴露工厂方法
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    singletonFactories.put(beanName, singletonFactory);
}

ObjectFactory的魔力

  • 不直接存储Bean对象
  • 存储一个能动态生成Bean早期引用的工厂
  • 工厂执行时机:首次发生循环依赖时

源码级解析:代理对象生成流程

java 复制代码
// AbstractAutowireCapableBeanFactory.java
protected Object doCreateBean(...) {
    // 1. 实例化原始对象
    Object beanInstance = instantiateBean(beanName, mbd);
    
    // 2. 关键!提前暴露ObjectFactory
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, beanInstance));
    
    // 3. 属性填充(可能触发循环依赖)
    populateBean(beanName, mbd, instanceWrapper);
}

// 生成早期引用(含代理逻辑)
protected Object getEarlyBeanReference(String beanName, ...) {
    Object exposedObject = bean;
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
        if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
            // 🔥 触发AOP代理创建(如AbstractAutoProxyCreator)
            exposedObject = ((SmartInstantiationAwareBeanPostProcessor) bp)
                .getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

工作流程(A→B→A + AOP代理场景):

  1. 创建A

    • 实例化原始A
    • 向三级缓存注册工厂:singletonFactories.put("A", ()->getEarlyBeanReference(A))
  2. 填充A属性时发现依赖B

    • 触发创建B
  3. 填充B属性时发现依赖A

    • 调用getSingleton("A")

      java 复制代码
      // DefaultSingletonBeanRegistry.java
      protected Object getSingleton(String beanName, boolean allowEarlyReference) {
          // 查一级缓存 -> 无
          Object singletonObject = this.singletonObjects.get(beanName);
          if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
              // 查二级缓存 -> 无(首次)
              singletonObject = this.earlySingletonObjects.get(beanName);
              if (singletonObject == null && allowEarlyReference) {
                  synchronized (this.singletonObjects) {
                      // 查三级缓存 -> 找到工厂!
                      ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                      if (singletonFactory != null) {
                          // 🔥 触发工厂方法:创建代理对象!
                          singletonObject = singletonFactory.getObject();
                          // 移入二级缓存
                          this.earlySingletonObjects.put(beanName, singletonObject);
                          this.singletonFactories.remove(beanName);
                      }
                  }
              }
          }
          return singletonObject;
      }
    • 此时生成A的代理对象,注入B中

  4. B初始化完成

    • 移入一级缓存singletonObjects
  5. A继续初始化

    • 完成属性填充(注入B)

    • 执行initializeBean()时尝试生成代理:

      java 复制代码
      // AbstractAutoProxyCreator.java
      public Object postProcessAfterInitialization(Object bean, String beanName) {
          if (bean != null) {
              Object cacheKey = getCacheKey(bean.getClass(), beanName);
              // 🔥 关键判断:若已提前代理,则跳过
              if (this.earlyProxyReferences.remove(cacheKey) != bean) {
                  return wrapIfNecessary(bean, beanName, cacheKey);
              }
          }
          return bean;
      }
    • 此时发现A已在循环依赖时被代理过,不再重复创建


四、为什么二级缓存无法解决AOP代理问题?

矛盾根源:代理创建的时序冲突

阶段 二级缓存方案 三级缓存方案
暴露时机 实例化后直接暴露原始对象 暴露生成代理的工厂方法
代理创建 初始化后创建代理 循环依赖发生时动态创建代理
一致性 依赖方拿到原始对象 依赖方直接拿到代理对象

二级缓存的致命缺陷:

  1. 提前暴露的是原始对象
    • 无法在暴露时确定是否需要代理(依赖后置处理器)
  2. 代理对象创建滞后
    • 导致循环依赖中注入对象与最终对象不一致
  3. 重复代理风险
    • 若在初始化阶段再次创建代理,内存中存在两个对象

五、高频面试题深度解析

Q1:无AOP场景下能否用二级缓存替代三级缓存?

可以,但Spring为统一设计保留三级结构

  • 三级缓存中ObjectFactory直接返回原始对象:

    java 复制代码
    addSingletonFactory(beanName, () -> bean); // 等同于二级缓存
  • 但Spring选择保持架构一致性,避免条件分支

Q2:为什么构造器注入无法用三级缓存解决?

根本原因:暴露时机太晚

java 复制代码
// 构造器注入流程
protected BeanWrapper createBeanInstance(...) {
    // 调用构造器时需要立即获得依赖Bean
    Constructor<?> constructorToUse = ...;
    // ❌ 此时尚未执行addSingletonFactory()!
    return BeanUtils.instantiateClass(constructorToUse, args);
}
  • 构造器注入必须在实例化阶段获得依赖
  • 而三级缓存在实例化之后才注册

Q3:Spring如何避免重复创建代理?

双重校验机制

  1. 提前代理标记

    java 复制代码
    // AbstractAutoProxyCreator.java
    public Object getEarlyBeanReference(Object bean, String beanName) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        // 标记该Bean已提前代理
        this.earlyProxyReferences.put(cacheKey, bean);
        return wrapIfNecessary(bean, beanName, cacheKey);
    }
  2. 初始化阶段跳过已代理Bean

    java 复制代码
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (this.earlyProxyReferences.remove(cacheKey) != bean) {
            // 仅对未提前代理的Bean创建代理
            return wrapIfNecessary(...);
        }
        return bean;
    }

六、设计启示录:三级缓存的本质

  1. 空间换时间

    • 通过三级结构换取循环依赖解决能力
  2. 关注点分离

    缓存层级 职责
    一级缓存 存储完全可用Bean
    二级缓存 避免重复创建早期引用
    三级缓存 动态解决代理一致性问题
  3. 开闭原则典范

    • ObjectFactory抽象允许扩展代理逻辑
    • 新增BeanPostProcessor不影响主流程

总结

Spring的三级缓存是代理场景下解决循环依赖的唯一方案

二级缓存因无法解决代理对象一致性 问题被淘汰,

一级缓存因无法隔离半成品Bean 被弃用。

该设计展现了Spring在架构妥协与严谨性间的完美平衡!

相关推荐
留不住丨晚霞15 分钟前
说说SpringBoot常用的注解?
java·开发语言
华科云商xiao徐21 分钟前
Java多线程爬虫动态线程管理实现
java·爬虫·数据挖掘
柒七爱吃麻辣烫30 分钟前
八股文系列-----SpringBoot自动配置的流程
java·spring boot·rpc
爱学习的茄子34 分钟前
React Hooks驱动的Todo应用:现代函数式组件开发实践与组件化架构深度解析
前端·react.js·面试
M1A135 分钟前
Java 面试系列第一弹:基础问题大盘点
java·后端·mysql
发仔12336 分钟前
Dubbo介绍及示例用法
java·dubbo
goxingman43 分钟前
关于使用idea打包的时候报错,Maven提示乱码java: �Ҳ�������
java·maven·intellij-idea
邓不利东2 小时前
Spring中过滤器和拦截器的区别及具体实现
java·后端·spring
草履虫建模3 小时前
Redis:高性能内存数据库与缓存利器
java·数据库·spring boot·redis·分布式·mysql·缓存
苹果醋33 小时前
Vue3组合式API应用:状态共享与逻辑复用最佳实践
java·运维·spring boot·mysql·nginx