Spring 为什么要用三级缓存?

张开发
2026/6/9 16:40:03 15 分钟阅读
Spring 为什么要用三级缓存?
Spring 为什么要用三级缓存一、从最根本的问题开始什么是循环依赖要理解 Spring 为什么需要三级缓存必须先从 Spring 要解决的核心问题出发。这个核心问题叫做循环依赖Circular Dependency。1.1 什么叫循环依赖在 Java 应用程序中我们会定义很多类这些类之间往往存在依赖关系。比如ComponentpublicclassA{AutowiredprivateBb;}ComponentpublicclassB{AutowiredprivateAa;}A 依赖 BB 依赖 A。这就形成了一个环形的依赖关系称为循环依赖。在现实业务中这种情况非常常见。Service 之间互相调用Manager 层和 Service 层互相引用这些都可能产生循环依赖。如果 Spring 不能处理这种情况那么大量正常的业务代码都无法运行。1.2 循环依赖会引发什么问题不使用 Spring直接用new来创建对象时循环依赖会导致无限递归最终程序崩溃栈溢出创建 A → A需要B → 创建B → B需要A → 创建A → A需要B → 创建B → ...无限循环Spring 作为一个成熟的 IoC 容器它的职责之一就是替你管理对象的创建和依赖注入。如果连循环依赖这种常见情况都无法处理那它就不是一个合格的框架。所以Spring 必须有一套机制来破解这个死循环。二、理解 Spring Bean 的创建过程要理解三级缓存的设计必须先深刻理解 Spring 创建一个 Bean 的完整过程。这是一切的基础。2.1 Bean 的创建分为两个阶段Spring 创建一个 Bean不是一步完成的而是分成两个阶段第一阶段实例化Instantiation调用构造方法在内存堆上分配空间生成一个对象。此时对象已经存在于内存中有了内存地址。但是对象内部的属性字段还都是默认值对象类型为 null基本类型为 0/false 等。这个对象被称为半成品 Bean或早期引用Early Reference。第二阶段初始化Initialization包括属性填充依赖注入、调用PostConstruct方法、调用InitializingBean.afterPropertiesSet()方法等。完成这个阶段后Bean 才是一个完整的、可以正常使用的对象。这个对象被称为成品 Bean。2.2 这两个阶段的区别为什么如此重要正是因为对象的创建可以分为先有对象实例化和再完善对象初始化这两步循环依赖才有了被解决的可能。我们再来看循环依赖的场景创建 A先实例化 AA 对象已存在内存中但 b 属性是 null填充 A 的属性发现需要 B于是去创建 B创建 B先实例化 BB 对象已存在内存中但 a 属性是 null填充 B 的属性发现需要 A关键时刻来了此时 A 虽然还没有完全初始化完毕b 属性还是 null但 A 这个对象已经实例化出来了它是真实存在于内存中的。我们可以把这个半成品的 A 提前暴露出去让 B 拿到这个 A 的引用完成对 B 的属性填充。然后 B 完成了初始化回头再完成 A 的初始化把 B 注入给 A。这个思路就是**“提前暴露”**是整个三级缓存设计的核心思想。三、缓存的必要性3.1 为什么要用缓存Spring 中的 Bean 默认是**单例Singleton**的即同一个类在整个 Spring 容器中只存在一个实例。这意味着同一个 Bean 不能被创建两次。当 Bean A 和 Bean B 都需要 Bean C 的时候它们拿到的必须是同一个 C 对象。如果没有缓存每次需要某个 Bean 时都直接创建那不仅会破坏单例语义还会引发循环依赖时的无限递归。所以 Spring 引入了缓存机制把已经创建好或正在创建中的 Bean 存储起来下次需要时直接从缓存中取而不是重新创建。3.2 缓存存在哪里Spring 的缓存存储在DefaultSingletonBeanRegistry这个类中它定义了三个关键的 Map// 一级缓存存放完全初始化好的 Bean成品 BeanprivatefinalMapString,ObjectsingletonObjectsnewConcurrentHashMap(256);// 二级缓存存放早期暴露的 Bean半成品 Bean已实例化但未完成初始化privatefinalMapString,ObjectearlySingletonObjectsnewHashMap(16);// 三级缓存存放 Bean 的工厂对象ObjectFactoryprivatefinalMapString,ObjectFactory?singletonFactoriesnewHashMap(16);这三个 Map 就是 Spring 的三级缓存。四、逐步推导为什么一级缓存不够二级缓存也不够需要三级缓存这是最核心的部分。我们来一步步推导看看每一级缓存解决了什么问题又留下了什么新问题。4.1 只有一级缓存能解决循环依赖吗假设我们只有一个缓存 Map叫singletonObjects里面混放成品 Bean 和半成品 Bean。流程如下开始创建 A实例化 A 得到半成品 A立刻将半成品 A 放入singletonObjects。填充 A 的属性发现需要 B去获取 B。singletonObjects中没有 B开始创建 B实例化 B 得到半成品 B立刻将半成品 B 放入singletonObjects。填充 B 的属性发现需要 A去获取 A。singletonObjects中有 A半成品直接返回给 B。B 完成属性填充完成初始化B 变成成品更新singletonObjects中 B 的记录。回到步骤 2A 拿到了完整的 B完成属性填充完成初始化A 变成成品更新singletonObjects中 A 的记录。表面上看这个流程是通的循环依赖被解决了。但是这里有一个严重的问题singletonObjects这个缓存在同一时刻既存放了半成品 Bean又存放了成品 Bean。这是极其危险的想象一下在步骤 3 完成后、步骤 7 完成之前如果有另外一个线程来获取 Bean A它从singletonObjects里拿到的是一个半成品的 A属性 b 还是 null它会直接使用这个不完整的对象这将导致不可预测的错误比如空指针异常、数据错误等等。所以成品 Bean 和半成品 Bean 绝对不能放在同一个缓存里。这就是为什么需要两个不同的缓存来区分它们。4.2 有了二级缓存能解决所有问题吗现在我们有两个缓存singletonObjects一级缓存只放完全初始化好的成品 Bean。earlySingletonObjects二级缓存放半成品 Bean早期暴露的 Bean。流程如下开始创建 A实例化 A 得到半成品 A将半成品 A 放入二级缓存earlySingletonObjects。填充 A 的属性发现需要 B去获取 B先查一级缓存没有再查二级缓存没有。开始创建 B实例化 B 得到半成品 B将半成品 B 放入二级缓存earlySingletonObjects。填充 B 的属性发现需要 A去获取 A先查一级缓存没有再查二级缓存有。从二级缓存中取出半成品 A注入给 B。B 完成初始化将成品 B 移入一级缓存singletonObjects从二级缓存中移除 B。回到步骤 2A 获取到了成品 B完成属性填充。A 完成初始化将成品 A 移入一级缓存singletonObjects从二级缓存中移除 A。这个方案完美解决了成品和半成品混放的问题线程安全性也得到了保障。那么既然二级缓存就能解决问题为什么还需要第三级缓存五、三级缓存存在的真正原因AOP5.1 AOP 是什么AOPAspect-Oriented Programming面向切面编程是 Spring 最重要的特性之一。在 Spring 中AOP 的实现原理是动态代理Spring 不直接给你原始的 Bean 对象而是给你一个代理对象Proxy Object。这个代理对象包裹着原始对象在你调用方法时代理对象可以在方法前后插入额外的逻辑比如事务管理、日志记录、权限验证等。举个例子ServiceTransactional// 声明式事务本质上是 AOPpublicclassUserService{AutowiredprivateOrderServiceorderService;publicvoidcreateUser(){// 业务逻辑}}当UserService上有Transactional注解时Spring 不会直接把UserService的实例放入容器而是会创建一个UserService的代理对象把这个代理对象放入容器。其他 Bean 注入的UserService实际上拿到的是这个代理对象。5.2 AOP 代理对象的创建时机正常情况下没有循环依赖时代理对象的创建发生在 Bean 完全初始化之后通过BeanPostProcessor具体是AbstractAutoProxyCreator来生成代理对象然后把代理对象放入一级缓存。关键代码逻辑简化版1. 实例化 Bean → 得到原始对象 2. 填充属性 3. 初始化调用各种 Aware 接口、init 方法等 4. 执行 BeanPostProcessor.postProcessAfterInitialization() → AOP 在这里生成代理对象 5. 将最终对象可能是代理对象放入一级缓存AOP 代理对象的生成是在第4步即 Bean 完全初始化之后。5.3 循环依赖 AOP 带来的问题现在假设 A 需要被 AOP 代理比如 A 上有Transactional同时 A 和 B 存在循环依赖。用二级缓存的方案来走一遍流程开始创建 A实例化 A得到原始的 A 对象非代理放入二级缓存。填充 A 的属性发现需要 B。开始创建 B实例化 B放入二级缓存。填充 B 的属性发现需要 A从二级缓存取出原始的 A 对象注入给 B。B 完成初始化执行 BeanPostProcessor如果 B 也需要代理B 被替换为代理 B放入一级缓存。回到 A 的属性填充A 拿到了 B或代理 B完成属性填充。A 完成初始化。执行 BeanPostProcessorA 被替换为代理 A将代理 A 放入一级缓存。问题出现了在第4步B 注入的是原始的 A 对象。在第8步一级缓存中存放的是代理 A 对象。这就造成了不一致B 持有的是原始 A而容器中管理的是代理 A。B 调用 A 的方法时直接绕过了 AOP 代理事务、日志等切面逻辑完全失效这是一个非常严重的 Bug。5.4 解决方案让半成品也能提前生成代理问题的根源在于当 B 需要注入 A 的时候A 的代理对象还没有被创建出来代理对象要等到 A 完全初始化后才生成。解决这个问题有一个思路当有人需要从二级缓存拿半成品 A 时不要直接放一个原始对象进去而是提前触发 AOP 代理逻辑生成代理 A然后把代理 A 放入二级缓存同时注入给 B。但是提前生成代理有一个前提条件你得知道A 这个 Bean 将来需不需要被代理。在 A 的实例化阶段Spring 并不一定已经做了这个判断。更重要的是并不是每次从二级缓存取半成品时都需要生成代理只有当这个半成品有可能被代理时才需要特殊处理。如果每次放入半成品时都立刻生成代理逻辑会变得非常复杂和耦合。Spring 的解决方案是引入第三级缓存存放一个工厂对象把是否需要生成代理以及如何生成代理这个决策延迟到真正被需要的那一刻。六、三级缓存的完整设计与工作原理6.1 三级缓存的职责定义现在我们来明确三级缓存各自的职责级别名称类型存放内容作用一级缓存singletonObjectsConcurrentHashMap完整的成品 Bean可能是代理对象对外提供完整可用的 Bean二级缓存earlySingletonObjectsHashMap早期暴露的对象可能是代理对象存放已提前暴露的半成品避免重复调用工厂三级缓存singletonFactoriesHashMapObjectFactory?工厂对象存放生成早期暴露对象的工厂是提前暴露的入口6.2 ObjectFactory 是什么ObjectFactory是一个函数式接口FunctionalInterfacepublicinterfaceObjectFactoryT{TgetObject()throwsBeansException;}它只有一个方法getObject()调用时会执行一段逻辑并返回一个对象。Spring 在 Bean 实例化完成后会立刻向三级缓存中放入一个ObjectFactory这个工厂的逻辑是// 伪代码实际在 AbstractAutowireCapableBeanFactory.doCreateBean() 中addSingletonFactory(beanName,()-getEarlyBeanReference(beanName,mbd,bean));其中getEarlyBeanReference方法会遍历所有的SmartInstantiationAwareBeanPostProcessor如果其中的 AOP 相关处理器判断这个 Bean 需要被代理就在此时提前生成代理对象并返回否则返回原始对象。这就是关键所在如果没有发生循环依赖这个ObjectFactory永远不会被调用代理对象按正常流程在初始化后生成。如果发生了循环依赖某个 Bean 需要提前获取当前 Bean 的引用时才会调用这个工厂此时提前生成代理对象。工厂只会被调用一次调用结果会被缓存到二级缓存中防止重复生成。6.3 完整流程详解A 和 B 循环依赖A 需要 AOP 代理现在我们用完整的三级缓存方案走一遍 A需要代理和 B 循环依赖的完整流程Step 1开始创建 ASpring 检查一级、二级、三级缓存均无 A。标记 A 为正在创建中放入singletonsCurrentlyInCreation集合。实例化 A得到原始 A 对象半成品。向三级缓存放入一个ObjectFactory() - getEarlyBeanReference(a, ..., 原始A对象)。三级缓存状态{ a: ObjectFactory_A }Step 2填充 A 的属性发现需要 B调用getBean(b)Spring 检查一级、二级、三级缓存均无 B。Step 3开始创建 B标记 B 为正在创建中。实例化 B得到原始 B 对象半成品。向三级缓存放入ObjectFactory_B。三级缓存状态{ a: ObjectFactory_A, b: ObjectFactory_B }Step 4填充 B 的属性发现需要 A调用getBean(a)。检查一级缓存无。检查二级缓存无。检查三级缓存有ObjectFactory_A调用ObjectFactory_A.getObject()执行getEarlyBeanReference。由于 A 需要 AOP 代理此时提前为 A 生成代理 A 对象注意这个代理 A 内部包裹的原始 A 此时 b 属性还是 null但代理对象本身已经创建好了。将代理 A放入二级缓存并从三级缓存中移除 A。缓存状态一级{}二级{ a: 代理A }三级{ b: ObjectFactory_B }将代理 A注入给 BB.a 代理A。Step 5B 完成初始化B 的所有属性填充完毕。执行 B 的各种初始化方法。执行BeanPostProcessor假设 B 不需要代理B 保持原始对象。将成品 B放入一级缓存从二级缓存如果有的话和三级缓存移除 B。缓存状态一级{ b: 成品B }二级{ a: 代理A }三级{}Step 6回到 A 的属性填充A 获取到了成品 B完成属性填充原始A.b 成品B。注意此时原始 A 的 b 属性填充完毕了。而代理 A 内部持有原始 A 的引用所以通过代理 A 调用方法时最终会委托给原始 A 执行而此时原始 A 的 b 属性已经正确填充了。Step 7A 完成初始化A 执行各种初始化方法。执行BeanPostProcessorpostProcessAfterInitialization。关键Spring 会检查二级缓存中是否已经有 A 的早期引用earlyProxyReferences记录了是否已提前代理。发现 A 已经在第4步被提前代理了所以不会再重复生成代理直接把二级缓存中的代理 A作为最终对象。将代理 A放入一级缓存从二级缓存移除 A。最终缓存状态一级{ a: 代理A, b: 成品B }二级{}三级{}Step 8最终结果验证容器中的 A 是代理 AAOP 有效容器中的 B 是成品 BB.a 持有的是代理 A和容器中的 A 是同一个对象✅代理 A 内部包裹的原始 A 的 b 属性是成品 B ✅所有引用一致AOP 正常工作循环依赖被完美解决七、二级缓存的存在意义你可能会问既然三级缓存工厂可以生成早期引用为什么还需要二级缓存直接用工厂每次现生成不行吗原因在于性能和一致性性能生成代理对象是有开销的需要使用 CGLIB 或 JDK 动态代理等技术。如果每次需要早期引用时都重新调用工厂生成既浪费性能又可能生成多个不同的代理对象实例。一致性单例保证如果 A 被 B 和 C 都依赖B 和 C 都与 A 循环依赖那么在 A 完成初始化之前B 和 C 都需要获取 A 的早期引用。如果每次都调用工厂会生成两个不同的代理对象这违反了单例原则B.a 和 C.a 不是同一个对象。有了二级缓存第一次调用工厂生成代理对象后将其存入二级缓存之后无论有多少个 Bean 需要这个早期引用都从二级缓存取同一个对象保证了单例。八、为什么 Spring 不能解决构造方法注入的循环依赖理解了三级缓存的原理后这个问题就很容易回答了。三级缓存解决循环依赖的关键前提是可以先实例化对象调用构造方法然后再进行属性注入。但是如果使用构造方法注入ComponentpublicclassA{privatefinalBb;publicA(Bb){// 构造方法注入this.bb;}}要调用 A 的构造方法必须先传入 B 的实例。但 B 还没有创建。要创建 BB 的构造方法可能又需要 A。A 还没创建完成卡在构造方法那里了。这就形成了真正的死锁无法通过提前暴露来解决因为连半成品都没有对象都没实例化出来。所以Spring无法解决构造方法注入导致的循环依赖遇到这种情况会直接抛出BeanCurrentlyInCreationException。九、总结三级缓存设计的核心逻辑通过完整的推导我们可以总结出 Spring 三级缓存的设计逻辑一级缓存singletonObjects的必要性存放完整成品 Bean是对外提供服务的最终缓存保证了 Bean 的单例性和完整性。二级缓存earlySingletonObjects的必要性成品和半成品必须隔离存放防止外部拿到未初始化完毕的 Bean。同时二级缓存保存了已经从工厂生成的早期引用避免重复生成保证单例。三级缓存singletonFactories的必要性这是整个方案的精髓。它不直接存储对象而是存储一个能够生成早期引用的工厂。这个设计实现了两个目标延迟性只有真正发生循环依赖时工厂才会被调用避免了不必要的代理对象提前生成。灵活性工厂中封装了是否需要 AOP 代理的判断逻辑使得循环依赖场景下也能正确生成代理对象保证了 AOP 功能的完整性。三个缓存共同协作解决了循环依赖和AOP 代理这两个问题同时存在时的终极难题。这也是 Spring 框架在设计上的一个精妙之处通过分层缓存将存放成品、“存放半成品”、延迟决策三个关注点清晰地分离开来以最小的复杂度解决了最棘手的问题。

更多文章