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)方式三:旁挂模式 二层组网(…...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...
什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...
C++.OpenGL (10/64)基础光照(Basic Lighting)
基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...
自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...
vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
关键领域软件测试的突围之路:如何破解安全与效率的平衡难题
在数字化浪潮席卷全球的今天,软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件,这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下,实现高效测试与快速迭代?这一命题正考验着…...
C#学习第29天:表达式树(Expression Trees)
目录 什么是表达式树? 核心概念 1.表达式树的构建 2. 表达式树与Lambda表达式 3.解析和访问表达式树 4.动态条件查询 表达式树的优势 1.动态构建查询 2.LINQ 提供程序支持: 3.性能优化 4.元数据处理 5.代码转换和重写 适用场景 代码复杂性…...
