连载中 3/18

三级缓存与循环依赖:为什么偏偏是三级

2026-11-03 · 57 阅读 · 0 评论 · 0 赞

死锁式造人:A 要 B,B 要 A

循环依赖的经典现场:OrderService 注入 UserService,UserService 又注入 OrderService。创建 A 发现缺 B,创建 B 发现缺 A——按直觉就是死锁。但 Spring 单例默认能转起来,靠的是提前曝光:A 刚实例化完(构造器跑完、属性还是空的),就把自己的引用先亮出来给 B 用。B 拿到的是「半成品 A」的引用——但引用是同一个内存地址,等 A 后续初始化完成,B 手里的引用指向的对象就自动「长成」了成品。变的是对象内部状态,不是引用本身,所以安全。

三个缓存,各司其职

一级 singletonObjects       成品池:初始化完成的单例
二级 earlySingletonObjects  半成品池:提前曝光的早期引用
三级 singletonFactories     工厂池:能产出早期引用的 ObjectFactory

取值按一、二、三级依次找。流程串起来:A 实例化后先把自己的工厂放进三级缓存 → 填充属性发现要 B → 创建 B → B 要 A → 从三级缓存找到 A 的工厂,调用它拿到早期引用,升级进二级缓存(三级删除)→ B 完成,进一级 → A 继续完成,进一级。注意关键细节:三级存的不是对象,是工厂——答案就埋在这。

为什么是三级,两级不够吗

二级缓存放早期引用,看似够用。但 Spring 有条设计原则:代理应该在初始化之后才生成(生命周期第 7 步)。若用二级缓存,就得在实例化后立刻生成代理放进去——所有 Bean 都被迫提前 AOP,哪怕绝大多数根本没有循环依赖。三级缓存的 ObjectFactory 是个延迟开关:只有真的发生循环依赖、别的 Bean 来要早期引用时,工厂才被调用(getEarlyBeanReference),此时才决定要不要提前生成代理;没有循环依赖,工厂永远不被调用,代理按正常流程在初始化后生成。三级缓存的本质:把「提前生成代理」这个特例的代价,推迟到确有必要时才付。

边界:三种救不了

其一,构造器注入——还没实例化就要依赖对方,根本没有「实例化后提前曝光」的机会,直接 BeanCurrentlyInCreationException;其二,原型 Bean——不进缓存,无曝光机制;其三,@Async 等在初始化后才生成代理的后置处理器——循环依赖提前曝光的引用与最终代理不是同一个,启动时直接报错拦截。另外要知道官方态度:Spring Boot 2.6 起默认禁止循环依赖,开启要显式配置 allow-circular-references=true——循环依赖是设计问题,不该靠框架无限兜底(第 12 篇细聊怎么改)。

一句话总结:三级缓存 = 成品池 + 半成品池 + 延迟生成代理的工厂池。提前曝光解决「鸡生蛋」,工厂延迟解决「代理时机」——两层设计各管一件事,缺一不可。下一篇讲贯穿生命周期与循环依赖的主角:BeanPostProcessor,Spring 魔法的实施者。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#Spring#三级缓存#循环依赖#Spring Boot

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10136 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9900 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9331 阅读 · 21 评论 · 287 赞