Apollo/Nacos配置动态刷新原理及优劣
一. 配置方式
这里只说与Spring集成后的配置方式,这也是项目中主要使用的方式
Apollo
- 在属性上直接加@value注解,这个属性就会随着配置的更改动态更新
- 类实现ConfigChangeListener,在类中方法上@ApolloConfigChangeListener注解,注解在方法上,监控配置的变化,配置变化后会自定义方法来达到动态刷新bean的目的
Nacos
- Nacos无需任何配置,即可对有@ConfigurationProperties注解的类进行配置的动态刷新
- Nacos自2022.0.0.0-RC1版本后可通过spring.cloud.nacos.config.refresh-bahavior指定刷新模式,默认是all_beans(刷新所有bean),可选specific_bean (只刷新有配置值更改的bean)
优劣对比
先说结论,二者的优劣势对比,具体的原理机制等相对复杂,放后文详细讲解
Apollo
优势
- 提供出listener,使用者可灵活自行定制bean的刷新方式
劣势
- Apollo除@Value注解外不提供动态刷新的默认实现方案,而@Value注解平常用的较少,就比较鸡肋,用户想要用@ConfigurationProperties配置类的方式动态刷新,必须要自己去实现
Nacos
优势
- 有动态刷新的默认实现,用户可直接使用,且从2022.0.0.0-RC1版本后可自选bean的刷新模式
劣势
- 不管选用哪种nacos的刷新方案,RefreshScope域都会全刷新,触发RefreshScopeRefreshedEvent事件的发布,如eureka会订阅该事件,并于该事件发布时,触发eurekaClient的重新注册,若是配置热更新的比较频繁,那么会触发eurekaClient的频繁重注册
最终抉择
- 动态热更新的时候,肯定是更新哪个配置,那么只将与这个配置对应的bean进行更新最好,若是用的nacos,那么可选用2022.0.0.0-RC1版本,bean的刷新行为选用specific_bean指定刷新,若是用的Apollo,需自己实现,也可参考nacos中的SmartConfigurationPropertiesRebinder类,进行自实现,配置动态刷新后,建议还是要发布下RefreshScopeRefreshedEvent事件,使得依赖该事件发布的其他组件,在配置刷新后,可重新配置与该之相关的的内容,避免真的更改了例如eureka的配置后,eureka因不能重注册client导致的配置无法生效的问题。这里比较坑的地方就是springcloud没有提供一种机制,可监听自己的配置更改及事件发布对应着触发事件,进行导致只是更改了用户自定义的一些配置也触发了eureka客户端重注册这种看似风马牛不相及的行为出现
动态刷新机制
这里只以nacos为例,重点讲解服务通过监听器拿到配置变更之后的流程,Apollo在自行实现时,也可参考此流程
- NacosContextRefresher监听ApplicationReadyEvent事件,会在应用准备启动时间发布后,注册NacosListener
- 在注册时,会实现listener的innerReceive方法,在配置变更后,会通知到该方法,该方法会触发事件的发布:
applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));
@Overridepublic void onApplicationEvent(ApplicationReadyEvent event) {// many Spring contextif (this.ready.compareAndSet(false, true)) {this.registerNacosListenersForApplications();}}/*** register Nacos Listeners.*/private void registerNacosListenersForApplications() {if (isRefreshEnabled()) {for (NacosPropertySource propertySource : NacosPropertySourceRepository.getAll()) {if (!propertySource.isRefreshable()) {continue;}String dataId = propertySource.getDataId();registerNacosListener(propertySource.getGroup(), dataId);}}}private void registerNacosListener(final String groupKey, final String dataKey) {String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);Listener listener = listenerMap.computeIfAbsent(key,lst -> new AbstractSharedListener() {@Overridepublic void innerReceive(String dataId, String group,String configInfo) {refreshCountIncrement();nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);// 这里发布RefreshEvent事件,用以刷新bean实例applicationContext.publishEvent(new RefreshEvent(this, null, "Refresh Nacos config"));if (log.isDebugEnabled()) {log.debug(String.format("Refresh Nacos config group=%s,dataId=%s,configInfo=%s",group, dataId, configInfo));}}});try {configService.addListener(dataKey, groupKey, listener);log.info("[Nacos Config] Listening config: dataId={}, group={}", dataKey,groupKey);}catch (NacosException e) {log.warn(String.format("register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,groupKey), e);}}
- bean刷新的处理,在订阅了该事件的RefreshEventListener中
public void handle(RefreshEvent event) {if (this.ready.get()) { // don't handle events before app is readylog.debug("Event received " + event.getEventDesc());Set<String> keys = this.refresh.refresh();log.info("Refresh keys changed: " + keys);}}
- 程序流转到ContextRefresher的refresh方法中
public synchronized Set<String> refresh() {// 刷新环境变量Set<String> keys = refreshEnvironment();// 刷新RefreshScope内的所有缓存this.scope.refreshAll();return keys;}
- 我们追溯到refreshEnvironment方法内,其内的重点有两处,一处是在这里获取到了配置中心更改的值,另一处,则将更改的值放入EnvironmentChangeEvent事件中进行发布
public synchronized Set<String> refreshEnvironment() {Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());addConfigFilesToEnvironment();Set<String> keys = changes(before,extract(this.context.getEnvironment().getPropertySources())).keySet();this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));return keys;
- 我们找到订阅EnvironmentChangeEvent该事件的类SmartConfigurationPropertiesRebinder,跟到onApplicationEvent方法,该方法会根据refreshBehavior选择对应的bean刷新方式
@Overridepublic void onApplicationEvent(EnvironmentChangeEvent event) {if (this.applicationContext.equals(event.getSource())// Backwards compatible|| event.getKeys().equals(event.getSource())) {switch (refreshBehavior) {case SPECIFIC_BEAN -> rebindSpecificBean(event);default -> rebind();}}}
- 我们先跟踪到默认处理方案:rebind方法中,该方法在当前类中的父类:ConfigurationPropertiesRebinder中实现
@ManagedOperationpublic void rebind() {this.errors.clear();for (String name : this.beans.getBeanNames()) {rebind(name);}}
- 跟踪到rebind(name)方法,可以看到在本方法中,对指定name的bean进行destory(销毁)并且重新initialize(实例化)
@ManagedOperationpublic boolean rebind(String name) {if (!this.beans.getBeanNames().contains(name)) {return false;}if (this.applicationContext != null) {try {Object bean = this.applicationContext.getBean(name);if (AopUtils.isAopProxy(bean)) {bean = ProxyUtils.getTargetObject(bean);}if (bean != null) {if (getNeverRefreshable().contains(bean.getClass().getName())) {return false; // ignore}this.applicationContext.getAutowireCapableBeanFactory().destroyBean(bean);this.applicationContext.getAutowireCapableBeanFactory().initializeBean(bean, name);return true;}}catch (RuntimeException e) {this.errors.put(name, e);throw e;}catch (Exception e) {this.errors.put(name, e);throw new IllegalStateException("Cannot rebind to " + name, e);}}return false;}
- 到这里我们就知道,原来在这里进行了bean的重新实例化,那么重新实例化的这些bean是什么bean呢,beans变量是关键线索,beans对应着ConfigurationPropertiesBeans类,当前类中注入了ConfigurationPropertiesBeans对象,我们跟踪到该对象,看该类的注释,可知该类是当前相聚中所有包含@ConfigurationProperties注解的bean的集合类
/*** Collects references to <code>@ConfigurationProperties</code> beans in the context and* its parent.**/
- 也就是说,在默认的情况下,随着配置的变更,会导致所有包含@ConfigurationProperties注解的bean重新绑定
- 接下来,我们回到第6步,进入SmartConfigurationPropertiesRebinder类的rebindSpecificBean(event)方法中
private void rebindSpecificBean(EnvironmentChangeEvent event) {Set<String> refreshedSet = new HashSet<>();beanMap.forEach((name, bean) -> event.getKeys().forEach(changeKey -> {String prefix = AnnotationUtils.getValue(bean.getAnnotation()).toString();// prevent multiple refresh one ConfigurationPropertiesBean.if (changeKey.startsWith(prefix) && refreshedSet.add(name)) {rebind(name);}}));}
- 可以看出该方法主要做的是事情是遍历beanMap,拿到对应的bean及其对应的前缀(etc: spring.xxx)然后再拿到变更的key,若匹配则才对当前bean进行更新,以此方法实现了只更新特定的bean,而不会像第10步一样全部更新
- 可以看出当前beanMap是核心,这个beanMap怎么得到的呢,可以看出它是在当前类构造函数中就已填充了
public SmartConfigurationPropertiesRebinder(ConfigurationPropertiesBeans beans) {super(beans);fillBeanMap(beans);}@SuppressWarnings("unchecked")private void fillBeanMap(ConfigurationPropertiesBeans beans) {this.beanMap = new HashMap<>();Field field = ReflectionUtils.findField(beans.getClass(), "beans");if (field != null) {field.setAccessible(true);this.beanMap.putAll((Map<String, ConfigurationPropertiesBean>) Optional.ofNullable(ReflectionUtils.getField(field, beans)).orElse(Collections.emptyMap()));}}
- 可看出它获取到父类的beans字段所对应值,而后采用反射最终取出ConfigurationPropertiesBeans类中的beans对象,放入当前的beanMap中
- 至此,对应第4步中的refreshEnvironment()方法执行完毕,接下来我们跟踪进入到RefreshScope的refreshAll方法中
@ManagedOperation(description = "Dispose of the current instance of all beans "+ "in this scope and force a refresh on next method execution.")public void refreshAll() {super.destroy();this.context.publishEvent(new RefreshScopeRefreshedEvent());}
- 我们跟踪进GenericScope的destory方法中
@Overridepublic void destroy() {List<Throwable> errors = new ArrayList<Throwable>();Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();for (BeanLifecycleWrapper wrapper : wrappers) {try {Lock lock = this.locks.get(wrapper.getName()).writeLock();lock.lock();try {wrapper.destroy();}finally {lock.unlock();}}catch (RuntimeException e) {errors.add(e);}}if (!errors.isEmpty()) {throw wrapIfNecessary(errors.get(0));}this.errors.clear();}
- 我们看到destroy方法会对当前的域缓存进行清空,若清空时返回数据,则会对缓存对应的bean进行destroy,之后会有其他地方对bean重新创建,这里也就是会对所有属于RefreshScope域的对象进行的重新实例化
- 在destroy执行后,会发布RefreshScopeRefreshedEvent事件,我们可以看查找所有订阅该事件的类,就可知有哪些组件会受此影响了
相关文章:
Apollo/Nacos配置动态刷新原理及优劣
一. 配置方式 这里只说与Spring集成后的配置方式,这也是项目中主要使用的方式 Apollo 在属性上直接加value注解,这个属性就会随着配置的更改动态更新类实现ConfigChangeListener,在类中方法上ApolloConfigChangeListener注解,注解…...
docker的基本管理
Docker的概念云计算三层架构服务说明应用IAAS基础设施及服务硬件(服务器、网络设置、防火墙等)虚拟化网络虚拟化(大二层)例:openstackPAAS平台及服务环境例:数据库、 docker 、kubernetesSAAS应用及服务应用…...
2023年房地产投资-租金和IRR研究报告
第一章 概况 房地产投资租赁是指置业投资者在购买到物业后,首先对该物业进行适当整饰与装修,之后以出租人的身份,以口头协议或签订合同的形式,将房屋交付承租人占有、使用与收益,由承租人向出租人交付租金的行为。通过…...
2023-2-10刷题情况
青蛙过河 题目描述 小青蛙住在一条河边, 它想到河对岸的学校去学习。小青蛙打算经过河里 的石头跳到对岸。 河里的石头排成了一条直线, 小青蛙每次跳跃必须落在一块石头或者岸上。 不过, 每块石头有一个高度, 每次小青蛙从一块石头起跳, 这块石头的高度就 会下降 1 , 当石头…...
Python学习-----无序序列2.0(集合的创建、添加、删除以及运算)
目录 前言: 什么是集合 集合的三大特性 1.集合的创建 (1)直接创建 (2)强制转换 2.集合的添加 (1)add()函数 (2)update() 函数 3.集合元…...
2023最详细的接口测试用例设计教程
一、接口测试流程 1、需求讨论 2、需求评审 3、场景设计 4、数据准备 5、测试执行 二、分析接口文档元素 1、接口名称 2、接口地址 3、支持格式 4、请求方式 5、请求参数(参数名称、类型、是否必填、参数说明等) 6、返回参数(返回…...
【数据库】 数据库的理论基础详解
目录 一, 什么是数据库 二, 数据库管理系统(DBMS) 三,数据库与文件系统的区别 1,对比区别: 2,优缺点总结: 四,数据库的发展史 五,常见数据库 1, 关系型…...
Linux环境运行Maven 生成的hadoop jar包
运行命令: hadoop jar ./jar包名字 class对象路径 输入路径 输出路径 linux内部jar包测试 cd 到以下目录,创建以下文件夹 [rootreagan180 ~]# cd /opt/soft/hadoop313/share/hadoop/mapreduce/ 创建文件夹(读取路径) [roo…...
ThreadPoolExecutor原理解析
1. 工作原理1.1 流程图1.2 执行示意图从上图得知如果当前运行的线程数小于corePoolSize(核心线程数),则会创建新线程作为核心线程来执行任务(注意,执行这一步需要获取全局锁)。如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQue…...
谷粒学苑第二章前端框架-2.2前端框架开发过程
一、前端框架开发过程 第一步:添加路由 src/router模块用来管理路由。 第二步:点击某个路由,显示路由对应页面内容 component: () > import(/views/table/index), 表示路由对应的页面,是views/table/index.vue页面 第三步&a…...
权限管理实现的两种方式(详解)
登录的接口请求的三个内容:1. token2. 用户信息、角色信息3. 菜单信息第一种:基于角色Role的动态路由管理 (不推荐,但市场用的比较多)首先列出枚举每个角色对应几个路由,然后根据用户登录的角色遍历枚举出来的角色动态注册对应的路…...
【C++】智能指针思路解析和模拟实现
此篇文章就从以下几个方面出发,带你了解智能指针的方方面面1.为什么需要智能指针当我们开辟内存并使用的时候,我们的顺序应该是这样:开辟内存-》使用内存-》释放内存问题就出现在第三步,开辟好了,也使用了,…...
SpringCloud(18):Sentinel流控降级入门
Sentinel本地应用流控降级实现分为三步: 创建本地应用搭建本地Sentinel控制台本地应用接入本地Sentinel控制台1 本地应用创建 整体流程分析 创建springboot项目在项目的pom.xml文件中引入sentinel-core的依赖坐标创建TestController,定义使用限流规则运行测试具体流程 1.创…...
C++【多态】
文章目录1、多态的概念2、多态的定义及实现2-1、多态的构成条件2-2、虚函数2-3、虚函数的重写2-4 多态样例2-5、协变2-6、 析构函数与virtual2-7、函数重载、函数隐藏(重定义)与虚函数重写(覆盖)的对比2-8、override 和 final&…...
缓存预热、缓存雪崩、缓存击穿、缓存穿透,你真的了解吗?
缓存穿透、缓存击穿、缓存雪崩有什么区别,该如何解决? 1.缓存预热 1.1 问题描述 请求数量较高,大量的请求过来之后都需要去从缓存中获取数据,但是缓存中又没有,此时从数据库中查找数据然后将数据再存入缓存…...
【Java基础】018 -- 面向对象阶段项目上(拼图小游戏)
目录 拼图小游戏(GUI) 一、主界面分析 1、练习一:创建主界面1 2、练习二:创建主界面2(JFrame) 3、练习三:在游戏界面中添加菜单(JMenuBar) ①、菜单的制作 4、添加图片&a…...
【网络~】
网络一级目录二、socket套接字三、UDP数据报套接字四、TCP流套接字一级目录 1.局域网、广域网 2.IP地址是什么? IP地址是标识主机在网络上的地址 IP地址是如何组成的? 点分十进制,将32位分为四个部分,每个部分一个字节ÿ…...
手写JavaScript中的call、bind、apply方法
手写JavaScript中的call、bind、apply方法 call方法 call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。 function Product(name, price) {this.name name;this.price price; }function Food(name, price) {Product.call(this, name, price);t…...
JAVA练习46-将有序数组转换为二叉搜索树
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 前言 提示:这里可以添加本文要记录的大概内容: 2月10日练习内容 提示:以下是本篇文章正文内容,下面案例可供参考 一、题目-…...
linux(centos7.6)docker
官方文档:https://docs.docker.com/engine/install/centos/1安装之前删除旧版本的docker2安装yum install-y yum-utils3配置yum源 不用官网的外国下载太慢 推荐阿里云yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.r…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...
Xshell远程连接Kali(默认 | 私钥)Note版
前言:xshell远程连接,私钥连接和常规默认连接 任务一 开启ssh服务 service ssh status //查看ssh服务状态 service ssh start //开启ssh服务 update-rc.d ssh enable //开启自启动ssh服务 任务二 修改配置文件 vi /etc/ssh/ssh_config //第一…...
阿里云ACP云计算备考笔记 (5)——弹性伸缩
目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...
Webpack性能优化:构建速度与体积优化策略
一、构建速度优化 1、升级Webpack和Node.js 优化效果:Webpack 4比Webpack 3构建时间降低60%-98%。原因: V8引擎优化(for of替代forEach、Map/Set替代Object)。默认使用更快的md4哈希算法。AST直接从Loa…...
基于Springboot+Vue的办公管理系统
角色: 管理员、员工 技术: 后端: SpringBoot, Vue2, MySQL, Mybatis-Plus 前端: Vue2, Element-UI, Axios, Echarts, Vue-Router 核心功能: 该办公管理系统是一个综合性的企业内部管理平台,旨在提升企业运营效率和员工管理水…...
如何应对敏捷转型中的团队阻力
应对敏捷转型中的团队阻力需要明确沟通敏捷转型目的、提升团队参与感、提供充分的培训与支持、逐步推进敏捷实践、建立清晰的奖励和反馈机制。其中,明确沟通敏捷转型目的尤为关键,团队成员只有清晰理解转型背后的原因和利益,才能降低对变化的…...
Python 高级应用10:在python 大型项目中 FastAPI 和 Django 的相互配合
无论是python,或者java 的大型项目中,都会涉及到 自身平台微服务之间的相互调用,以及和第三发平台的 接口对接,那在python 中是怎么实现的呢? 在 Python Web 开发中,FastAPI 和 Django 是两个重要但定位不…...
Qt学习及使用_第1部分_认识Qt---Qt开发基本流程
前言 学以致用,通过QT框架的学习,一边实践,一边探索编程的方方面面. 参考书:<Qt 6 C开发指南>(以下称"本书") 标识说明:概念用粗体倾斜.重点内容用(加粗黑体)---重点内容(红字)---重点内容(加粗红字), 本书原话内容用深蓝色标识,比较重要的内容用加粗倾…...
C++参数传递 a与a的区别
在 C 中,&a(引用)和 a(值传递) 的关键区别在于 参数如何传递给函数,以及由此引发的 性能、语义和安全问题。 最核心的在于你想不想传入的参数被改变,如果想,就用参数传递&#…...
Modbus转ETHERNET IP网关:快速冷却系统的智能化升级密钥
现代工业自动化系统中,无锡耐特森Modbus转Ethernet IP网关MCN-EN3001扮演着至关重要的角色。通过这一技术,传统的串行通讯协议Modbus得以在更高速、更稳定的以太网环境中运行,为快速冷却系统等关键设施的自动化控制提供了强有力的支撑。快速冷…...
