三级缓存到底缓存了什么:一次 @Transactional 代理把循环依赖炸在启动阶段的复盘
title: 三级缓存到底缓存了什么一次 Transactional 代理把循环依赖炸在启动阶段的复盘tags: [Spring, 循环依赖, 三级缓存, 源码分析, Java]category: 后端一个周三早上订单服务起不来了我们组的订单服务在测试环境发布后启动失败控制台甩出这么一段org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name orderServiceImpl: Bean with name orderServiceImpl has been injected into other beans [couponServiceImpl] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean.关键词是最后半句其他 bean 拿到的不是最终版本的 bean。改动本身很小OrderServiceImpl上加了一个Transactional因为要把「扣库存 写订单」放进同一个事务。加之前一切正常加之后启动直接崩。当时组里第一反应是「Spring 不是能自动解决循环依赖吗」于是就有了这次把三级缓存从头翻了一遍的排查。Spring Boot 版本 2.3.12Spring Framework 5.2.15JDK 8。这个组合很关键后面会说为什么。先把现场还原出来去掉业务逻辑后最小复现代码是这样Service public class OrderServiceImpl implements OrderService { // 构造器之外的字段注入Spring 允许循环依赖 Autowired private CouponService couponService; Override Transactional(rollbackFor Exception.class) // 罪魁祸首在这一行 public void createOrder(Long userId, Long skuId) { couponService.deduct(userId); // ... 写订单 } } Service public class CouponServiceImpl implements CouponService { Autowired private OrderService orderService; // 反向依赖闭环形成 Override public void deduct(Long userId) { // ... 扣券 } }逐行看这段代码的要害第 5 行Autowired是字段注入。Spring 只对字段/setter 注入的循环依赖提供了补救手段构造器注入的循环依赖从来救不了。第 9 行Transactional会让OrderServiceImpl在初始化后被 AOP 包成一个代理对象。加上这行之后容器里最终存的不再是那个原始对象。第 18 行的反向注入让两个 bean 形成闭环谁先创建谁就要走「提前暴露」的流程。问题就出在CouponServiceImpl在半路拿到的是OrderServiceImpl的原始对象引用而容器最后放进去的是代理对象。两个引用对不上Spring 检测到不一致直接抛异常。三级缓存到底是哪三级翻DefaultSingletonBeanRegistry的源码三个 Map 的定义就在类顶部public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry { /** 一级缓存完全初始化好的成品 bean */ private final MapString, Object singletonObjects new ConcurrentHashMap(256); /** 三级缓存bean 的工厂用来按需生成早期引用 */ private final MapString, ObjectFactory? singletonFactories new HashMap(16); /** 二级缓存从三级工厂产出的早期引用可能已经是代理 */ private final MapString, Object earlySingletonObjects new HashMap(16); protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject this.singletonObjects.get(beanName); if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null allowEarlyReference) { ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { singletonObject singletonFactory.getObject(); // 关键调用 this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return singletonObject; } }这段代码值得逐行读查找顺序是一级 → 二级 → 三级逐级降级。命中一级说明 bean 已完全就绪直接返回。isSingletonCurrentlyInCreation是循环依赖的判定开关只有当这个 bean 正在创建中才允许去翻二三级缓存。singletonFactory.getObject()是整个机制的核心它不是简单地返回原始对象而是执行一段可能产生代理的逻辑。产出后立刻把结果挪到二级缓存并删掉三级工厂保证同一个 bean 的早期引用只生成一次多次注入拿到的是同一个对象。很多人背八股背成「一级放成品、二级放半成品、三级放工厂」就停了。真正该问的是为什么不能只用两级答案就在getObject()里。三级缓存的真正用途给 AOP 留后门三级缓存里放的工厂是在doCreateBean里注册的// AbstractAutowireCapableBeanFactory#doCreateBean 片段 boolean earlySingletonExposure (mbd.isSingleton() this.allowCircularReferences isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { // 注册三级工厂注意这里传的是 lambda不是对象本身 addSingletonFactory(beanName, () - getEarlyBeanReference(mbd, beanName, bean)); } // 填充属性此处触发对 couponService 的注入进而递归创建 CouponServiceImpl populateBean(beanName, mbd, instanceWrapper); // 执行初始化AOP 在这里正常织入 exposedObject initializeBean(beanName, exposedObject, mbd); protected Object getEarlyBeanReference(RootBeanDefinition mbd, String beanName, Object bean) { Object exposedObject bean; for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp (SmartInstantiationAwareBeanPostProcessor) bp; // AbstractAutoProxyCreator 在这里提前把 bean 包成代理 exposedObject ibp.getEarlyBeanReference(exposedObject, beanName); } } return exposedObject; }理解要点addSingletonFactory传进去的是lambda 而非对象。也就是说「要不要生成代理」这个决定被延后了——没有循环依赖就永远不执行有循环依赖才执行。getEarlyBeanReference会走一遍SmartInstantiationAwareBeanPostProcessorAOP 的AbstractAutoProxyCreator正是在这里把 bean 提前包成代理。如果只有两级缓存直接把原始对象放进二级那么所有bean 在被提前暴露时都得先判断要不要代理等于把 AOP 时机整体前移破坏了「初始化后织入」的语义。三级缓存的价值就是这个「延迟决策」。到这里我们的报错为什么会发生也就清楚了正常情况下getEarlyBeanReference提前生成的代理会被记住最后exposedObject会和它保持一致。而我们那次的问题在于——OrderServiceImpl上还挂了一个自定义的BeanPostProcessor它在postProcessAfterInitialization里又包了一层自定义代理导致最终对象和早期暴露的代理不是同一个。Spring 在doCreateBean尾部做一致性校验时发现对不上就抛了那个异常。排查过程中最耗时间的两小时说实话最初我们完全跑偏了。因为异常信息里带着couponServiceImpl我们花了将近两小时在优惠券服务上找问题把它的依赖树画了一遍甚至怀疑是 Feign 客户端的代理。真正的转折点是加了一行调试Component public class BeanRefDebugger implements ApplicationListenerContextRefreshedEvent { Autowired private ApplicationContext ctx; Override public void onApplicationEvent(ContextRefreshedEvent event) { Object order ctx.getBean(orderServiceImpl); CouponServiceImpl coupon ctx.getBean(CouponServiceImpl.class); // 打印两处引用的 identityHashCode看是不是同一个对象 System.out.println(容器中的 order System.identityHashCode(order) , class order.getClass().getName()); System.out.println(coupon 持有的 order System.identityHashCode(ReflectionTestUtils.getField(coupon, orderService))); } }打印结果一目了然容器里的是OrderServiceImpl$$EnhancerBySpringCGLIB$$xxx而couponServiceImpl手里握的是另一个 hash 的对象。问题从来不在优惠券服务在我们自己那个多余的 BeanPostProcessor 上。这个教训后来写进了组内的排查手册遇到循环依赖报错先别看异常里点名的那个 bean先用identityHashCode把「容器里的」和「被注入的」两个引用打出来比一比比读一小时源码管用。几种解法的真实取舍方案改动量副作用我的态度spring.main.allow-circular-referencestrue一行配置把设计问题掩盖Spring Boot 2.6 默认关它是有道理的不建议只当临时止血Lazy注入一个注解注入的是代理首次调用才初始化调试栈变深可接受适合救火ObjectProviderT延迟获取改几行代码语义清晰无隐式代理推荐比Lazy显式抽公共逻辑到第三个 bean改动最大需要重新划分职责长期方案我们最后选的这个改用 setter 注入绕开构造器循环中等只是绕过环还在不解决根因我们最终的处理是把OrderServiceImpl里调用优惠券的那段抽成OrderCouponFacade让两个 service 都依赖它环直接断掉。改完之后不仅启动正常链路也清爽了——之前那个环本身就是职责划分没做好的信号。如果只是想快速上线ObjectProvider是我更推荐的临时方案Service public class CouponServiceImpl implements CouponService { private final ObjectProviderOrderService orderServiceProvider; public CouponServiceImpl(ObjectProviderOrderService orderServiceProvider) { // 构造器里只存 provider不触发 OrderService 的创建 this.orderServiceProvider orderServiceProvider; } Override public void deduct(Long userId) { // 真正用到时才去容器里取此时对方已经是完整的代理对象 OrderService orderService orderServiceProvider.getObject(); orderService.markCouponUsed(userId); } }这段的好处是构造器注入的语义保住了依赖仍然是 final 的同时把「取实例」的时机推到方法调用时环在启动阶段就不成立了。相比Lazy读代码的人一眼能看出这里做了延迟不需要去猜注解背后的行为。复盘数据几个真实数字从报错到定位根因2 小时 40 分钟其中 2 小时浪费在错误的 bean 上。涉及的 bean 数量整个环只有 2 个类但被间接牵连的自动装配 bean 有17 个。升级到 Spring Boot 2.6.6 后同样的代码启动阶段直接被allow-circular-references默认关闭拦下报错更早、信息更明确。2.6 那个「不友好」的默认值其实帮我们提前暴露了问题。重构后订单服务启动时间从 18.4s 降到 16.9s虽然不是重构目标但少了一层多余代理确实有收益。我的判断三级缓存是一套为已有设计缺陷兜底的补救机制不是可以依赖的特性。Spring 官方从 2.6 开始默认禁止循环依赖态度已经很明确了。我不建议在新代码里靠allow-circular-references过日子。它能让你今天上线但会让下一个人在某个周三早上对着BeanCurrentlyInCreationException发呆。真正划算的做法是看到环先怀疑职责划分而不是先找 Spring 的开关。反过来说如果你维护的是一个五年以上的老系统几十个 bean 缠在一起那么理解三级缓存的意义就完全不同了——它是你在不重构的前提下继续活下去的知识储备。这类系统里ObjectProvider加注释说明是我见过性价比最高的处理方式。留个问题如果OrderServiceImpl改成构造器注入CouponService三级缓存还救得了吗为什么提示想想addSingletonFactory是在实例化之后、属性填充之前调用的而构造器注入需要在实例化那一刻就拿到依赖。欢迎在评论区写下你的答案也欢迎贴出你遇到过的最离谱的一个循环依赖现场。

相关新闻

Beagle高级技巧:动态布局与数据更新的最佳实践

Beagle高级技巧:动态布局与数据更新的最佳实践

Beagle高级技巧:动态布局与数据更新的最佳实践 【免费下载链接】beagle A framework to help implement Server-Driven UI in your apps natively. 项目地址: https://gitcode.com/gh_mirrors/be/beagle Beagle是一款基于Server-Driven UI概念的开源跨平台框…

2026/8/4 22:06:52 阅读更多 →
深度解析:如何用DyberPet框架构建你的专属桌面宠物应用

深度解析:如何用DyberPet框架构建你的专属桌面宠物应用

深度解析:如何用DyberPet框架构建你的专属桌面宠物应用 【免费下载链接】DyberPet Desktop Cyber Pet Framework based on PySide6 项目地址: https://gitcode.com/GitHub_Trending/dy/DyberPet 桌面宠物应用开发正迎来新的技术浪潮,DyberPet作为…

2026/8/4 22:06:52 阅读更多 →
Win10/11蓝屏报错PFN_LIST_CORRUPT怎么处理?从驱动到内存的排查修复步骤

Win10/11蓝屏报错PFN_LIST_CORRUPT怎么处理?从驱动到内存的排查修复步骤

Win10/11 系统突然蓝屏并提示 PFN_LIST_CORRUPT,很多用户第一反应是重装系统。其实这个错误码背后通常指向驱动异常、内存条故障、硬盘坏道或系统文件损坏这几类问题。本文按从易到难的顺序,梳理一套完整的排查与修复流程,其中会用到「软领驱…

2026/8/4 22:06:52 阅读更多 →

最新新闻

做 TWS 应用开发,到底需不需要懂协议栈?

做 TWS 应用开发,到底需不需要懂协议栈?

刚接触 TWS 应用开发时,我一直有一个疑问:蓝牙协议栈大部分已经由芯片原厂封装好了,平时做功能主要是调用 SDK 接口、处理事件和编写产品逻辑,还有没有必要专门学习协议栈? 刚入行的时候,我也尝试过从头学习…

2026/8/4 22:59:20 阅读更多 →
Claude Code 很火,但真正能提效的只有这一类人

Claude Code 很火,但真正能提效的只有这一类人

如果你正准备往大模型方向转,《别急着上Claude Code,先把成本、边界和失败兜底算清楚》这类问题别只看热度。更重要的是判断自己该补哪块能力,以及怎么证明你真的会。摘要最近群里很多人问 Claude Code 怎么用,我直接说结论&#…

2026/8/4 22:59:20 阅读更多 →
探索BOPBTL-scratch-detection-fp32-mlx:微软老照片修复技术的MLX实现

探索BOPBTL-scratch-detection-fp32-mlx:微软老照片修复技术的MLX实现

探索BOPBTL-scratch-detection-fp32-mlx:微软老照片修复技术的MLX实现 【免费下载链接】BOPBTL-scratch-detection-fp32-mlx 项目地址: https://ai.gitcode.com/hf_mirrors/mlx-community/BOPBTL-scratch-detection-fp32-mlx BOPBTL-scratch-detection-fp32…

2026/8/4 22:58:19 阅读更多 →
测试转大模型:Demo 跑通了,权限日志才是真实门槛

测试转大模型:Demo 跑通了,权限日志才是真实门槛

聊《同样转大模型,测试背景的优势和短板分别是什么?》之前,先说一句实在的:别急着背概念,先看它在真实项目里到底解决什么问题。 摘要 摘要:从功能测试转做 AI 测试,很多人以为学会了写 promp…

2026/8/4 22:58:19 阅读更多 →
学习action笔记

学习action笔记

假设有个叫外卖的场景 打电话给餐厅:"给我做个三明治,做好了叫我。"餐厅说:"我不知道你的电话号码,没法叫你。"因为没有 Action,餐厅不知道做好后怎么通知你。如果有Action:// 餐厅的菜单 publi…

2026/8/4 22:58:19 阅读更多 →
从安装到推理:mlx-optiq驱动Laguna-XS-2.1-OptiQ-4bit的完整使用教程

从安装到推理:mlx-optiq驱动Laguna-XS-2.1-OptiQ-4bit的完整使用教程

从安装到推理:mlx-optiq驱动Laguna-XS-2.1-OptiQ-4bit的完整使用教程 【免费下载链接】Laguna-XS-2.1-OptiQ-4bit 项目地址: https://ai.gitcode.com/hf_mirrors/mlx-community/Laguna-XS-2.1-OptiQ-4bit Laguna-XS-2.1-OptiQ-4bit是一款基于mlx-optiq技术构…

2026/8/4 22:58:19 阅读更多 →

日新闻

AI Agent白手起家26: 使用标准事件驱动大模型实践

AI Agent白手起家26: 使用标准事件驱动大模型实践

纲要 练习目标:掌握大模型标准事件的调用回顾 LangChain 中的核心标准事件 invokestreambatchastream_eventswith_structured_output 环境准备实战代码:多种事件调用对比 同步调用与流式输出批量处理异步事件流监听结构化输出 运行说明与预期结果总结与扩…

2026/8/4 0:00:40 阅读更多 →
dealsea是什么?跨境卖家必知的美国deal站入门指南

dealsea是什么?跨境卖家必知的美国deal站入门指南

说实话,第一次听说美国这个老牌折扣网站的跨境卖家,十个有八个会问同一个问题:这个平台到底是干嘛的?我见过一个做家居出口的朋友,他在亚马逊上月销二十万美金,却从来没用过它。我给他看了首页——一屏一屏…

2026/8/4 0:01:40 阅读更多 →
清华大学重磅EST:植物自导电闪蒸焦耳热600°C/2600°C两步法!稀土超积累植物秒级转化为CeO₂-石墨烯电催化剂!

清华大学重磅EST:植物自导电闪蒸焦耳热600°C/2600°C两步法!稀土超积累植物秒级转化为CeO₂-石墨烯电催化剂!

通讯作者:邓兵、刘建国通讯单位:清华大学DOI:https://doi.org/10.1021/acs.est.6c00603研究背景稀土元素(REEs)是清洁能源技术与电子器件不可或缺的核心原料,然而传统提取方式依赖能耗高、排放大的采矿与强…

2026/8/4 0:01:40 阅读更多 →

周新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/4 13:24:41 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/4 11:41:39 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/4 5:26:40 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/4 13:38:24 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/4 11:09:16 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/4 13:38:40 阅读更多 →