JetCache启动循环依赖分析
问题呈现
项目性能优化,需要将本地内存(JVM内存)替换为本地Redis(同一个Pod中的Container),降低JVM内存和GC的压力,同时引入了JetCache简化和统一使用(对JetCache也做了扩展,支持了本地Redis)。引入JetCache后,项目启动就会报循环依赖的错误,错误入口则是项目中使用@PostConstruct调用JetCache加载预热缓存的地方,错误堆栈如下:
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'globalCacheConfig' defined in class path resource [cn/jojo/edu/jetcache/autoconfigure/JetCacheAutoConfiguration.class]: Circular depends-on relationship between 'globalCacheConfig' and 'redissonAutoInit'at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:305) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:227) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1155) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:416) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at cn.jojo.edu.jetcache.anno.field.LazyInitCache.init(LazyInitCache.java:83) ~[jetcache-anno-ss2.6.9-20241025.105436-2.jar:?]at cn.jojo.edu.jetcache.anno.field.LazyInitCache.checkInit(LazyInitCache.java:66) ~[jetcache-anno-ss2.6.9-20241025.105436-2.jar:?]at cn.jojo.edu.jetcache.anno.field.LazyInitCache.put(LazyInitCache.java:159) ~[jetcache-anno-ss2.6.9-20241025.105436-2.jar:?]at cn.jojo.edu.malacca.integration.cache.AbstractLocalRedisCacheService.put2Cache(AbstractLocalRedisCacheService.java:92) ~[classes/:?]at cn.jojo.edu.malacca.integration.cache.AbstractLocalRedisCacheService.forceRefreshCache(AbstractLocalRedisCacheService.java:140) ~[classes/:?]at cn.jojo.edu.malacca.api.server.config.PreheatInit.execute(PreheatInit.java:96) ~[classes/:?]at cn.jojo.edu.malacca.api.server.config.PreheatInit.lambda$init$0(PreheatInit.java:73) ~[classes/:?]at cn.jojo.infra.sdk.context.request.RequestContextHolder.setContext(RequestContextHolder.java:162) ~[microservice-sdk-1.7.29.jar:?]at cn.jojo.edu.malacca.api.server.config.PreheatInit.init(PreheatInit.java:71) ~[classes/:?]at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0_65]at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0_65]at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0_65]at java.lang.reflect.Method.invoke(Method.java:497) ~[?:1.8.0_65]at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:389) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:333) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:157) ~[spring-beans-5.2.4.RELEASE.jar:5.2.4.RELEASE]... 18 more
看错误日志问题是出在预热调用LazyInitCache.put方法时,该方法内部首先会获取GlobalCacheConfig对象,让其优先初始化,就是在初始化该对象时报的循环依赖错误:
存在以下几个奇怪的问题:
- 错误提示的是globalCacheConfig和redissonAutoInit产生了循环依赖,但是看GlobalCacheConfig本身并没有依赖其它复杂的对象,只是在初始化时依赖了SpringConfigProvider、JetCacheProperties和AutoConfigureBeans,globalCacheConfig和redissonAutoInit的依赖是如何产生的呢?
- 为什么在@PostConstruct中预热会报错,但在@EventListener(ApplicationReadyEvent.class)中预热却不会报错?
- 为什么配置启用了多个缓存组件才会报错,只启用一个缓存组件却不会?
- Spring通过三级缓存解决了非构造函数注入产生的循环依赖,那为什么这个循环依赖没有被解决呢?
分析过程
依赖关系分析
RedissonAutoInit对GobalCacheConfig的依赖比较好发现,从图中可以看到是父类中引用了ConfigProvider,在ConfigProvider中又引用了GlobalCacheConfig,不过从图中也无法找出GobalCacheConfig对RedissonAutoInit的依赖从何而来。
于是查看报循环依赖错误的源码:
这部分源码位于AbstractBeanFactory.doGetBean方法中,在创建Bean之前会对有设置dependsOn属性的BeanDefinition(以下简称BD)校验是否有循环依赖,但是GlobalCacheConfig类中并没有标记@DependsOn注解,还有哪里可以设置呢?不难想到Spring本身提供的一些扩展点是可以修改BD属性的,比如BeanFactoryPostProcessor和BeanDefinitionRegistryPostProcessor,最终找到一个BeanFactoryPostProcessor的实现类BeanDependencyManager:
public class BeanDependencyManager implements BeanFactoryPostProcessor {@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {String[] autoInitBeanNames = beanFactory.getBeanNamesForType(AbstractCacheAutoInit.class, false, false);if (autoInitBeanNames != null) {BeanDefinition bd = beanFactory.getBeanDefinition(JetCacheAutoConfiguration.GLOBAL_CACHE_CONFIG_NAME);String[] dependsOn = bd.getDependsOn();if (dependsOn == null) {dependsOn = new String[0];}int oldLen = dependsOn.length;dependsOn = Arrays.copyOf(dependsOn, dependsOn.length + autoInitBeanNames.length);System.arraycopy(autoInitBeanNames,0, dependsOn, oldLen, autoInitBeanNames.length);bd.setDependsOn(dependsOn);}}}
在这里将获取到的AbstractCacheAutoInit的子类都设置到了GlobalCacheConfig的dependsOn属性中(题外话这里并不是所有实现了AbstractCacheAutoInit的子类都会设置进去,还要看是否满足条件,Jetache提供了JetCacheCondition,只有配置启用了的缓存组件对应的Init才会初始化并设置到dependsOn中)。
至此,循环依赖就产生了。
循环依赖错误根因分析
有了循环依赖,但为什么Spring的三级缓存没有解决该问题,以及为什么是在某些条件下才会报错呢?本节就对剩余几个问题进行分析。
上面的时序图是在启用了Caffeine和Redisson缓存且使用@PostConstruct预热缓存的前提下的启动初始化过程,首先BeanDependencyManager获取到CaffeineAutoInit和RedissonAutoInit对象并设置到GlobalCacheConfig BD的dependsOn属性中。接着在@PostConstruct标记的方法执行,调用put预热缓存,在put中会优先调用getBean初始化GlobalCacheConfig。
GlobalCacheConfig初始化时就会进入到这段代码,循环依次判断依赖的对象是否有循环依赖以及调用getBean对其进行初始化。
首先是CaffeineAutoInit,在判断无循环依赖后会注册CaffeineAutoInit -> GlobalCacheConfig的依赖关系,表示CaffeineAutoInit依赖GlobalCacheConfig对象(下文同理);注册依赖关系后调用getBean初始化CaffeineAutoInit时其实也会进入到这个方法,只不过该对象没有dependsOn,所以直接跳过了这段代码,进入到实例化的逻辑:
熟悉Spring初始化逻辑的就知道该方法中创建完Bean就会对其进行依赖注入,而CaffeineAutoInit的父类中依赖了SpringConfigProvider对象:
因此又会实例化SpringConfigProvider对象,同样的该对象中又依赖了GlobalCacheConfig,所以又再次触发GlobalCacheConfig的创建,所以会再一次判断GlobalCacheConfig depnedsOn的对象和自己是否有循环依赖。
因为CaffeineAutoInit在前面已经初始化完成了,所以这次只是简单判断一下,接着判断并初始化RedissonAutoInit,这里又会注册RedissonAutoInit -> GlobalCacheConfig的依赖关系,然后又依赖注入SpringConfigProvider,依赖注入完成后会注册SpringConfigProvider -> RedissAutoInit的依赖关系(这里就是导致报错的关键步骤)。
到这里,GlobalCacheConfig注入到SpringConfigProvider完成,进入到图中第六步操作,注册GlobalCacheConfig -> SpringConfigProvider依赖关系,紧接着SpringConfigProvider注入到CaffeineAutoInit完成,注册SpringConfigProvider -> CaffeineAutoInit依赖关系。然后又一次判断并初始化RedissonAutoInit:
private boolean isDependent(String beanName, String dependentBeanName, @Nullable Set<String> alreadySeen) {if (alreadySeen != null && alreadySeen.contains(beanName)) {return false;}String canonicalName = canonicalName(beanName);Set<String> dependentBeans = this.dependentBeanMap.get(canonicalName);if (dependentBeans == null) {return false;}if (dependentBeans.contains(dependentBeanName)) {return true;}for (String transitiveDependency : dependentBeans) {if (alreadySeen == null) {alreadySeen = new HashSet<>();}alreadySeen.add(beanName);if (isDependent(transitiveDependency, dependentBeanName, alreadySeen)) {return true;}}return false;}
dependentBeanMap就是存储依赖关系的容器,canonicalName则是globalCacheConfig,dependentBeanName是redissonAutoInit,所以dependentBeans就是springConfigProvider,递归再次判断就有了R -> S -> G - R的循环依赖。
看到这,对于剩余几个问题就很容易理解了,下面总结概括下:
当在@PostConstruct中调用put方法保存缓存时,会优先创建初始化GlobalCacheConfig对象,该对象存在dependsOn依赖的对象,Spring会对有dependsOn的BD在实例化之前判断是否有循环依赖,当dependsOn只有一个时,三级缓存帮我们解决了循环依赖的问题,不会报错;而当有多个时,实例化第一个依赖的对象CaffeineAutoInit时,会导致GlobalCacheConfig的嵌套创建,在第二次创建时,第二个依赖对象RedissonAutoInit会注册依赖关系,返回到首次创建的地方再次判断RedissonAutoInit的依赖关系,就得到循环依赖的结果。而当GlobalCacheConfig延迟主动实例化时(只要在它之前随便先创建一个Init对象),直接就从单例缓存中就获取到了对象(依赖注入会创建该对象并放入缓存),进入不到这个判断,所以就不会报错(依赖注入触发的GlobalCacheConfig实例化虽然会进入这个判断,但不会导致嵌套创建),感兴趣的可以自行画一下创建过程。
最后还有个问题,为什么Spring要在createBean之前对有dependsOn属性的BD判断是否有循环依赖,是为了应对什么场景?没有这个判断,针对这个场景三级缓存是否也能解决循环依赖的问题呢?
相关文章:

JetCache启动循环依赖分析
问题呈现 项目性能优化,需要将本地内存(JVM内存)替换为本地Redis(同一个Pod中的Container),降低JVM内存和GC的压力,同时引入了JetCache简化和统一使用(对JetCache也做了扩展&#x…...

【科研绘图】3DMAX管状图表生成插件TubeChart使用方法
3DMAX管状图表生成插件TubeChart,一款用于制作3D管状图表的工具。可以自定义切片的数量以及随机或指定切片颜色。 【版本要求】 3dMax 2008及更高版本 【安装方法】 TubeChart插件无需安装,使用时直接拖动插件脚本文件到3dMax视口中打开即可࿰…...

基于SSM土家风景文化管理系统的设计
管理员账户功能包括:系统首页,个人中心,用户管理,景点分类管理,热门景点管理,门票订单管理,旅游线路管理,系统管理 前提账号功能包括:系统首页,个人中心&…...
C++超强图片预览器
下载 文件打开关联 关键代码 uint32_t getSrcPx3(const cv::Mat& srcImg, int srcX, int srcY, int mainX, int mainY) const {cv::Vec3b srcPx = srcImg.at<cv::Vec3b>(srcY, srcX);intUnion ret = 255;if (curPar.zoomCur < curPar.ZOOM_BASE && src…...

网络搜索引擎Shodan(2)
声明:学习视频来自b站up主 泷羽sec,如涉及侵权马上删除文章 声明:本文主要用作技术分享,所有内容仅供参考。任何使用或依赖于本文信息所造成的法律后果均与本人无关。请读者自行判断风险,并遵循相关法律法规。 感谢泷…...
【Tableau】
Tableau 是一款强大且广泛使用的数据可视化和商业智能(BI)工具,用于帮助用户分析、探索和呈现数据。它通过直观的拖放界面,允许用户轻松创建动态仪表板和报告,而无需编写代码。Tableau 可处理多种数据源,如…...

分类与有序回归
分类问题 分类问题,例如分类猫、狗、猪时,使用数字进行表示为1,2,3。而1、2、3之间有大小,分类算法为了平衡标签之间的差异,使得损失公平,会使用one-hot编码。例如,分别使用&#x…...

Mac如何实现高效且干净的卸载应用程序
使用Mac卸载应用程序,你还在使用废纸篓这个办法吗,看不见卸载了什么,看不见清理了多少,真的不会有残留吗 XApp Mac上的卸载专家,强大的垃圾逻辑检测,垃圾扫描更全面,卸载更干净 使用简单&#…...
LaTex中的常用空格命令
【LaTex中的常用空格命令】 在 LaTeX 中,有几个常用的空格指令: ● \,:一个小空格,通常用于在数学公式中插入较小的间距。● \quad:一个等宽空格,相当于当前字体尺寸下的字符宽度。 ● \qquad:两…...

k8s 1.28.2 集群部署 Thanos 对接 MinIO 实现 Prometheus 数据长期存储
文章目录 [toc]什么是 ThanosThanos 的主要功能Thanos 的架构组件Thanos 部署架构SidecarReceive架构选择 开始部署部署架构创建 namespacenode-exporter 部署kube-state-metrics 部署Prometheus Thanos-Sidecar 部署固定节点创建 label生成 secretMinIO 配置etcd 证书 启动 P…...

域渗透AD渗透攻击利用 python脚本攻击之IPC连接 以及 python生成exe可执行程序讲解方式方法
Python脚本批量检测ipc连接 import os, timeips [192.168.1.121,192.168.1.8 ] users {administrator,hack,hack1,test, } passs {123qq.com,456qq.com,Admin12345 } for ip in ips:for user in users:for mima in passs:exec1 "net use \\" "\\" i…...

行为设计模式 -命令模式- JAVA
命令模式 一.简介二. 案例2.1 接收者(Receiver)2.2 命令接口实现对象(ConcreteCommand)2.3 调用者( invoker)2.4 获取Receiver对象2. 5 装配者客户端测试 三. 结论3.1 要点3.2 示例 前言 本设计模式专栏写了…...
使用redis实现发布订阅功能及问题
如何使用redis实现发布订阅及遇到的问题 使用背景: 服务A通过接口操作服务B,实现相应逻辑。生产环境上,服务A有两个pod,服务B有3个pod 通过接口调用时,请求只能打到服务B的一个pod上,而我们想要的是服务B的…...
Debug日程工作经验总结日程常用
数据库 db连接命令 kubectl exec -it -n de dbs-53-cdf57d8dd-l4l29 sh su - postgres psql psql -h 10.115.19.118 -p 12080 -U postgres -d clouddb SET search_path TO “h.com”; select * from ems_ice limit 1; 也可以不切换schema,直接sql查询 select * f…...

Apache Paimon主键表的一些最佳实践
今天我们说说Paimon主键表的一些使用上的注意事项。 一、主键表 主键表是Paimon的一种表类型。用户可以插入、更新或删除表中的记录。 说的直白点就是,允许你设置唯一主键,然后覆盖更新。 Bucket选择 无论分区表还是未分区表,Bucket都是最小的…...
React面试常见题目(基础-进阶)
React面试常见题目及详细回答讲解 基础题目(20个) 什么是React? 回答:React是一个用于构建用户界面的JavaScript库,它允许你将UI拆分成可复用的组件。React起源于Facebook的内部项目,用于构建高性能的Web应…...

AI赋能:开启你的副业创业之路
随着人工智能(AI)技术的迅猛发展,越来越多的人开始探索与之相关的副业机会。AI不仅深刻改变了我们的工作和生活方式,还为愿意学习和运用这项技术的人们打开了丰富的创业和增收之门。今天,我们就来盘点几条与AI相关的副…...
前端文件上传组件流程的封装
1. 前端文件上传流程 选择文件: 用户点击上传按钮,选择要上传的文件。使用 <input type"file"> 或 FileReader API 读取文件。 文件校验: 校验文件的大小、格式等信息,提前过滤掉不符合要求的文件,避免…...
图像篡改研究
使用生成对抗网络 (GAN) 来篡改已有的图片涉及生成和修改图像的技术。以下是如何使用GAN对现有图像进行篡改的详细步骤: 1. 选择合适的GAN模型 不同类型的GAN模型适用于不同的图像处理任务。以下是几个常见的GAN模型及其应用: CycleGAN:用…...
wlan的8种组网方式的区别
1)方式一:直连模式 二层组网(直接转发/ 集中转发) (2)方式二:直连模式 三层组网(集中转发) (3)方式三:旁挂模式 二层组网(…...
KubeSphere 容器平台高可用:环境搭建与可视化操作指南
Linux_k8s篇 欢迎来到Linux的世界,看笔记好好学多敲多打,每个人都是大神! 题目:KubeSphere 容器平台高可用:环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...

手游刚开服就被攻击怎么办?如何防御DDoS?
开服初期是手游最脆弱的阶段,极易成为DDoS攻击的目标。一旦遭遇攻击,可能导致服务器瘫痪、玩家流失,甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案,帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
ubuntu搭建nfs服务centos挂载访问
在Ubuntu上设置NFS服务器 在Ubuntu上,你可以使用apt包管理器来安装NFS服务器。打开终端并运行: sudo apt update sudo apt install nfs-kernel-server创建共享目录 创建一个目录用于共享,例如/shared: sudo mkdir /shared sud…...
Python爬虫实战:研究feedparser库相关技术
1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...

云原生安全实战:API网关Kong的鉴权与限流详解
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 一、基础概念 1. API网关(API Gateway) API网关是微服务架构中的核心组件,负责统一管理所有API的流量入口。它像一座…...

莫兰迪高级灰总结计划简约商务通用PPT模版
莫兰迪高级灰总结计划简约商务通用PPT模版,莫兰迪调色板清新简约工作汇报PPT模版,莫兰迪时尚风极简设计PPT模版,大学生毕业论文答辩PPT模版,莫兰迪配色总结计划简约商务通用PPT模版,莫兰迪商务汇报PPT模版,…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...