Spring 的三级缓存与循环依赖

在 Spring 的日常开发中,循环依赖(Circular Dependency)是一个绕不开的话题。当 Bean A 依赖 Bean B,而 Bean B 又反过来依赖 A 时,如果没有特殊的处理机制,容器在创建对象时就会陷入无限递归的死锁。

然而,Spring 却能在绝大多数场景下优雅地化解这一危机。其背后的核心功臣,便是大名鼎鼎的"三级缓存"机制。但如果你深入探究,往往会产生一连串的疑问:为什么非要三级?两级不行吗?AOP 代理在其中扮演了什么角色?

本文将带你拨开源码的迷雾,用通俗的逻辑和代码链路,彻底讲透三级缓存的设计哲学。

1. Spring 的"破局"三板斧

Spring 解决这个问题的核心思路是:"先上车,后补票"。它允许一个对象在还未完全创建好(比如属性还没填完)时,就提前暴露一个"半成品"引用,让依赖它的其他对象先拿着用,等自己真正完成创建后,再"补全"自己。

为了实现这个精妙的机制,Spring 在 DefaultSingletonBeanRegistry 中准备了三层缓存,它们各司其职:

  • 一级缓存 (singletonObjects):成品库,存放完全初始化好的 Bean 实例。
  • 二级缓存 (earlySingletonObjects):半成品库,存放提前暴露的、但还未完成属性填充和初始化的 Bean 实例。
  • 三级缓存 (singletonFactories):工厂库,存放 ObjectFactory 对象工厂,用于按需生成半成品引用或 AOP 代理。

为了让你直观地看到它们是如何协同工作的,我们以经典的 A 依赖 B、B 依赖 A 为例,来看一张时序图:

sequenceDiagram participant A as AService (创建中) participant B as BService (创建中) participant L1 as 一级缓存 (成品) participant L2 as 二级缓存 (半成品) participant L3 as 三级缓存 (工厂) Note over A: 1. 开始创建 A A->>A: 实例化 (new 对象) A->>L3: 2. 将自身工厂放入三级缓存 Note over A: 3. 开始属性填充,发现依赖 B A->>B: 4. 去获取 B Note over B: 5. 开始创建 B B->>B: 实例化 (new 对象) B->>L3: 6. 将自身工厂放入三级缓存 Note over B: 7. 开始属性填充,发现依赖 A B->>L1: 8. 获取 A (一级缓存未命中) B->>L2: 9. 获取 A (二级缓存未命中) B->>L3: 10. 获取 A 的工厂并执行 L3-->>B: 11. 返回 A 的早期引用(半成品) B->>L2: 12. 将 A 的半成品放入二级缓存 B->>L3: 13. 从三级缓存移除 A 的工厂 Note over B: 14. B 的属性填充完成 Note over B: 15. B 执行初始化 B->>L1: 16. 将完整的 B 放入一级缓存 Note over A: 17. A 获取到完整的 B,继续填充 Note over A: 18. A 执行初始化 A->>L1: 19. 将完整的 A 放入一级缓存

看到这里,你可能和我一样,脑子里会冒出第一个疑问。

2. 常见疑问一:为什么二级缓存不够用?

等等,从流程图上看,A 实例化后直接把半成品放到二级缓存,B 不就能直接从二级缓存拿到 A 了吗?那三级缓存是不是多余的?"

答案是:单看解决循环依赖,二级缓存确实足够了。 但三级缓存的存在,是为了解决一个更棘手的问题------AOP 代理。

如果没有三级缓存,只在二级缓存中存放原始对象,那么当 A 被 AOP 增强后(比如有 @Transactional 注解),B 依赖的 A 应该是代理对象,而不是原始对象。如果二级缓存中只存了原始对象,B 就拿不到正确的代理对象,导致事务等功能失效。

如果为了解决这个问题,把二级缓存改成"实例化后立即检查是否要生成 AOP 代理,需要就生成代理再放进去",那又会引发新的问题:每一个 Bean 在实例化后都要执行一遍 AOP 检查,这对绝大多数没有循环依赖的 Bean 来说,是一种性能浪费。

这就是三级缓存设计的精妙之处:它存储的是一个 ObjectFactory(对象工厂),而不是最终对象。这个工厂只在 "真正发生循环依赖" 时(即其他 Bean 来获取早期引用时)才会被触发执行,从而按需、延迟地创建 AOP 代理。没有循环依赖的 Bean,就不会触发这个工厂,代理会照常在其生命周期后期生成。

3. 常见疑问二:那我把"工厂"和"结果"合并到一个缓存里不行吗?

"既然三级缓存是工厂,二级缓存是工厂的执行结果,那为什么不合二为一?缓存里如果没有值,就当场执行工厂逻辑拿到对象再返回,不也行吗?"

这是一个非常深入的思考,我们刚才也重点探讨过。这个方案在逻辑上完全可行,但 Spring 最终选择了"分离"而不是"合并",主要出于以下考虑:

  • 单一职责:三级缓存只管"怎么造"(工厂),二级缓存只管"造出来什么"(结果)。职责清晰,代码可读性极高。
  • 并发安全:在并发场景下,如果有两个线程同时来获取 A,Spring 通过从三级缓存中移除工厂(remove 操作)这一原子动作,确保了工厂逻辑有且仅有一个线程能成功执行并生成对象。如果工厂和结果混在一个 Map 里,你就需要用额外的锁或状态标记来防止重复执行,逻辑会变得复杂且容易出错。

所以,Spring 的设计是典型的用空间(多一个 Map)换清晰度和并发安全性,这是一种非常优雅的架构权衡。

4. 总结

Spring 的三级缓存,是一个层层递进、逻辑严密的解决方案:

  • 一级缓存 是最终成品库,确保单例。
  • 二级缓存 是半成品库,也是三级缓存执行结果的缓存,保证循环依赖中大家拿到的是同一个对象。
  • 三级缓存 是工厂库,实现了 AOP 代理的按需、延迟创建,避免了不必要的性能开销。

这套机制在保证高性能的同时,优雅地解耦了 Spring 的 IoC 容器和 AOP 等高级功能,无愧于 Spring 框架最精妙的设计之一。

相关推荐
子兮曰1 小时前
AI Coding Method Map:一张图看懂 AI 编程的完整链路
前端·人工智能·后端
Chenyiax1 小时前
从 PyTorch Attention 源码理解 KV Cache、缓存命中与 Prefix Cache
后端
IT_陈寒2 小时前
React状态更新总是不及时?你可能漏了这步批处理机制
前端·人工智能·后端
Jinkey3 小时前
要用户手机号真的是为了打骚扰电话吗?浅谈微信生态会员账号体系与资产合并
后端·微信·微信小程序
葫芦和十三3 小时前
图解 MongoDB 06|模式演进:无 schema 是优势还是债
后端·mongodb·agent
葫芦和十三11 小时前
图解 MongoDB 05|文档模型设计:内嵌 vs 引用,反范式不是免费午餐
后端·mongodb·agent
不能放弃治疗14 小时前
单 Agent 实现模式
后端
IT_陈寒16 小时前
Redis内存爆了,原来我漏掉了这个致命配置
前端·人工智能·后端
fliter17 小时前
最后一块拼图:用 bitvec 构造 IPv4 包,真正做出自己的 Ping
后端