Apollo/Nacos配置动态刷新原理及优劣
创始人
2024-05-25 05:32:11
0

一. 配置方式

这里只说与Spring集成后的配置方式,这也是项目中主要使用的方式

Apollo

  1. 在属性上直接加@value注解,这个属性就会随着配置的更改动态更新
  2. 类实现ConfigChangeListener,在类中方法上@ApolloConfigChangeListener注解,注解在方法上,监控配置的变化,配置变化后会自定义方法来达到动态刷新bean的目的

Nacos

  1. Nacos无需任何配置,即可对有@ConfigurationProperties注解的类进行配置的动态刷新
  2. 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在自行实现时,也可参考此流程

  1. NacosContextRefresher监听ApplicationReadyEvent事件,会在应用准备启动时间发布后,注册NacosListener
  2. 在注册时,会实现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);}}
  1. 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 keys = this.refresh.refresh();log.info("Refresh keys changed: " + keys);}}
  1. 程序流转到ContextRefresher的refresh方法中
	public synchronized Set refresh() {// 刷新环境变量Set keys = refreshEnvironment();// 刷新RefreshScope内的所有缓存this.scope.refreshAll();return keys;}
  1. 我们追溯到refreshEnvironment方法内,其内的重点有两处,一处是在这里获取到了配置中心更改的值,另一处,则将更改的值放入EnvironmentChangeEvent事件中进行发布
	public synchronized Set refreshEnvironment() {Map before = extract(this.context.getEnvironment().getPropertySources());addConfigFilesToEnvironment();Set keys = changes(before,extract(this.context.getEnvironment().getPropertySources())).keySet();this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));return keys;
  1. 我们找到订阅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();}}}
  1. 我们先跟踪到默认处理方案:rebind方法中,该方法在当前类中的父类:ConfigurationPropertiesRebinder中实现
	@ManagedOperationpublic void rebind() {this.errors.clear();for (String name : this.beans.getBeanNames()) {rebind(name);}}
  1. 跟踪到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;}
  1. 到这里我们就知道,原来在这里进行了bean的重新实例化,那么重新实例化的这些bean是什么bean呢,beans变量是关键线索,beans对应着ConfigurationPropertiesBeans类,当前类中注入了ConfigurationPropertiesBeans对象,我们跟踪到该对象,看该类的注释,可知该类是当前相聚中所有包含@ConfigurationProperties注解的bean的集合类
/*** Collects references to @ConfigurationProperties beans in the context and* its parent.**/
  1. 也就是说,在默认的情况下,随着配置的变更,会导致所有包含@ConfigurationProperties注解的bean重新绑定
  2. 接下来,我们回到第6步,进入SmartConfigurationPropertiesRebinder类的rebindSpecificBean(event)方法中
	private void rebindSpecificBean(EnvironmentChangeEvent event) {Set 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);}}));}
  1. 可以看出该方法主要做的是事情是遍历beanMap,拿到对应的bean及其对应的前缀(etc: spring.xxx)然后再拿到变更的key,若匹配则才对当前bean进行更新,以此方法实现了只更新特定的bean,而不会像第10步一样全部更新
  2. 可以看出当前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) Optional.ofNullable(ReflectionUtils.getField(field, beans)).orElse(Collections.emptyMap()));}}
  1. 可看出它获取到父类的beans字段所对应值,而后采用反射最终取出ConfigurationPropertiesBeans类中的beans对象,放入当前的beanMap中
  2. 至此,对应第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());}
  1. 我们跟踪进GenericScope的destory方法中
	@Overridepublic void destroy() {List errors = new ArrayList();Collection 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();}
  1. 我们看到destroy方法会对当前的域缓存进行清空,若清空时返回数据,则会对缓存对应的bean进行destroy,之后会有其他地方对bean重新创建,这里也就是会对所有属于RefreshScope域的对象进行的重新实例化
  2. 在destroy执行后,会发布RefreshScopeRefreshedEvent事件,我们可以看查找所有订阅该事件的类,就可知有哪些组件会受此影响了

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...