当前位置: 首页 > 科技观察

Nacos或者Config如何实现配置热刷新?

时间:2023-03-14 00:03:39 科技观察

本文转载自微信公众号《Java大厂面试官》,作者laker。转载本文请联系Java大厂面试官公众号。Laker前言题1.如何实现配置热刷新1.@RefreshScope原理2.ContextRefresher.refresh()3.RefreshScope.refreshAll()4.模拟造轮题2.Nacos客户端如何监听配置更新实时Nacos服务器1.Apollo实现方法2.什么是DeferredResult3.模拟造轮子总结前言文章简单介绍了实现技术要点,以及如何模拟造一个简单的轮子(造轮子很重要,只有当你想到造轮子的时候,你才会问有很多理论问题),具体源码详情请google文中关键词,然后跟着debug。问题一、如何实现配置热刷新keyNacos原理:1、在需要热刷新的Bean上使用SpringCloud原生注解@RefreshScope2、当有配置更新时,调用contextRefresher.refresh()代码如下:@RestController@RequestMapping("/config")@RefreshScope//keypublicclassConfigController{@Value("${laker.name}")//待刷新的属性privateStringlakerName;@RequestMapping("/get")publicStringget(){returnlakerName;}...}1。@RefreshScope原理@RefreshScope位于spring-cloud-context中,源码注释如下:@Bean定义可以放到org.springframework.cloud.context.scope.refresh.RefreshScope中。以这种方式注释的Bean可以在运行时刷新,任何使用它们的组件都将获得一个新实例,该实例已完全初始化并在下一个方法调用之前注入所有依赖项。要理解RefreshScope,首先要理解ScopeScope(org.springframework.beans.factory.config.Scope)是Spring2.0的核心概念。RefreshScope(org.springframework.cloud.context.scope.refresh),即@Scope("refresh")是springcloud提供的一种特殊scope实现,用于实现配置和实例热加载。类似的还有:RequestScope:从当前web请求中获取的实例SessionScope:从Session中获取的实例ThreadScope:从ThreadLocal中获取的实例RefreshScope是从内置缓存中获取的。2.ContextRefresher.refresh()当有配置更新时,触发ContextRefresher.refreshRefreshScope刷新过程入口为ContextRefresher.refreshpublicsynchronizedSetrefresh(){①Mapbefore=extract(this.context.getEnvironment().getPropertySources());②updateEnvironment();④Setkeys=changes(before,③extract(this.context.getEnvironment().getPropertySources())).keySet();⑤this.context.publishEvent(newEnvironmentChangeEvent(this.context,keys));⑥this.scope.refreshAll();}①提取除标准参数(SYSTEM,JNDI,SERVLET)以外的所有参数变量②将原Environment中的参数放入新的SpringContext容器中并重新加载,完成后关闭新容器大功告成(重点:可以去debug跟踪,其实是重启了一个SpringApplication)③调出更新后的参数(不包括标准参数)④对比变化项⑤发布环境变化事件生成Bean的过程很简单.清除refreshscope缓存并销毁Bean。下一次,将从BeanFactory中获取一个新实例(该实例使用新配置)。3.RefreshScope.refreshAll()RefreshScope.refreshAll方法的实现,即上面第⑥步调用:publicvoidrefreshAll(){super.destroy();this.context.publishEvent(newRefreshScopeRefreshedEvent());}RefreshScope类有一个成员变量cache,用于缓存所有生成的bean。使用get方法时,尽量从缓存中加载。如果没有,则生成一个新的对象放入缓存,通过getBean初始化其对应的Bean:;this.locks.putIfAbsent(name,newReentrantReadWriteLock());try{returnvalue.getBean();}catch(RuntimeExceptione){this.errors.put(name,e);throwe;}}所以销毁的时候只需要清空整个缓存,下次拿到对象的时候,自然可以重新生成一个新的对象,自然绑定一个新的属性:publicvoiddestroy(){Listerrors=newArrayList();Collectionwrappers=this.cache.clear();for(BeanLifecycleWrapperwrapper:wrappers){try{Locklock=this.locks.get(wrapper.getName()).writeLock();lock.lock();try{wrapper.destroy();}finally{lock.unlock();}}catch(RuntimeExceptione){errors.add(e);}}if(!errors.isEmpty()){throwwrapIfNecessary(errors.get(0));}this.errors.clear();}清除缓存后,下次访问该对象时,会重新创建一个新的对象放入缓存中。清除缓存后,它还会发送一个RefreshScopeRefreshedEvent事件,一些SpringCloud组件会监听这个事件并做出一些反馈。4.模拟造轮这里我们可以模拟造一个热更新轮;代码及配置如下:项目依赖spring-cloud-contextorg.springframework.cloudspring-cloud-context配置bean@Component@RefreshScopepublicclassUser{@Value("${laker.name}")privateStringname;...}刷新界面和查看界面@RestController@RequestMapping("/config")publicclassConfigController{@AutowiredUseruser;@AutowiredContextRefreshercontextRefresher;@RequestMapping("/get")publicStringget(){returnuser.getName();}@RequestMapping("/refresh")publicString[]refresh(){Setkeys=contextRefresher.refresh();returnkeys.toArray(newString[keys.size()]);}application.ymllaker:name:laker运行过程如下:1.浏览器http://localhost:8080/config/get-浏览器结果:laker2。修改application.yml中的内容为:laker:name:lakerupdate3。浏览器http://localhost:8080/config/refresh-浏览器结果:laker.name4。浏览器http://localhost:8080/config/get-浏览器结果:lakerupdate(未重复新启动,配置更新已经实现)问题2.Nacos客户端如何实时监控Nacos服务器的配置更新?这里可以查看Nacos源码。它使用长轮询。什么是长轮询和其他替代协议?RocketMQNacosApolloKafka花了几个小时看了Nacos长轮询的源码。有太多看不懂了。感兴趣的可以自行google。一般我们都是以SpringBoot为背景。各种google之后,发现实现了Apollo。比较简单,直接参考Apollo的代码。1、Apollo的实现方法如下:客户端会向ConfigService的notifications/v2接口,即NotificationControllerV2发起Http请求。如果秒内没有客户端关心的配置发布,则返回Http状态码304给客户端。如果有客户端关心的配置发布,NotificationControllerV2会调用DeferredResult的setResult方法传入配置变化的命名空间信息,请求立即返回。客户端从返回结果中获取配置变更的命名空间后,会立即向ConfigService请求获取该命名空间的最新配置。理解:关键字DeferredResult,利用该特性实现长轮询超时返回时,返回的状态码为HttpCode304解释:自上次请求以来,请求的网页没有被修改过。服务器返回此响应时,不返回网页内容,节省带宽和开销。2.什么是DeferredResult异步支持是在Servlet3.0引入的,简单来说就是允许HTTP请求在请求接收者线程之外的另一个线程中处理。自Spring3.2起可用的DeferredResult有助于将长时间运行的计算从http-worker线程卸载到单独的线程。虽然另一个线程会占用一些资源来做计算,但工作线程不会被阻塞并且可以处理传入的客户端请求。异步请求处理模型非常有用,因为它有助于在高负载期间很好地扩展应用程序,特别是对于IO密集型操作。DeferredResult是对异步Servlet的封装。具体可以参考我在CSDN上写的SpringBoot。使用DeferredResult实现长轮询这里用网上的一张图会更清楚。Servlet异步流程图收到请求后,tomcat工作线程从HttpServletRequest中获取一个异步上下文AsyncContext对象,然后tomcat工作线程将AsyncContext对象传递给业务处理线程,同时tomcat工作线程返回到工作线程池。这一步是异步启动的。在业务处理线程中完成业务逻辑的处理,生成响应返回给客户端。3、模拟造轮这里我们使用SpringBoot简单模拟一下如何通过SpringBootDeferredResult实现长轮询服务推送。代码如下,仅供参考:/***模拟ConfigService通知客户端的长轮询实现原理*/@RestController@RequestMapping("/config")publicclassLakerConfigController{privatefinalLoggerlogger=LoggerFactory.getLogger(this.getClass());//guava中Multimap,多值map,map的增强,一键可存多个值privateMultimap>watchRequests=Multimaps.synchronizedSetMultimap(HashMultimap.create());/***模拟长轮询*/@RequestMapping(value="/get/{dataId}")publicDeferredResultwatch(@PathVariable("dataId")StringdataId){logger.info("Requestreceived");ResponseEntityNOT_MODIFIED_RESPONSE=newResponseEntity<>(HttpStatus.NOT_MODIFIED);//超时时间为30s,返回304状态码,告诉客户端当前命名空间的配置文件没有更新DeferredResultdeferredResult=newDeferredResult<>(30*1000L,NOT_MODIFIED_RESPONSE);//当tdeferredResult完成(无论是超时、异常还是正常完成),去掉watchRequests中对应的watchkeydeferredResult.onCompletion(()->{logger.info("removekey:"+dataId);watchRequests.remove(dataId,deferredResult);});deferredResult.onTimeout(()->{logger.info("onTimeout()");});watchRequests.put(dataId,deferredResult);logger.info("Servletthreadreleased");returndeferredResult;}/***模拟发布配置*/@RequestMapping(value="/update/{dataId}")publicObjectpublishConfig(@PathVariable("dataId")StringdataId){if(watchRequests.containsKey(dataId)){Collection>deferredResults=watchRequests.get(dataId);Longtime=System.currentTimeMillis();//通知所有watch本次命名空间变化的长轮训配置变化结果for(DeferredResultdeferredResult:deferredResults){//一旦deferredResult执行了setResult()方法,就说明这个DeferredResult正常完成了,马上返回结果给客户端deferredResult.setResult(dataId+"changed:"+time);}}return"success";}}运行过程如下:为了简单起见,我用浏览器模拟,实际使用JavaHttp客户端,如:okhttp、Apachehttp客户端等正常进程:client1浏览器http://localhost:8080/config/get/laker,阻塞client2浏览器http://localhost:8080/config/update/laker,返回成功cessclient1browserhttp://localhost:8080/config/get/laker,returnlakerchanged:1611022736865超时过程:client1browserhttp://localhost:8080/config/get/laker,blocking30safterclient1browsedDevice,returnhttp代码304此处插入图片描述总结Nacos使用长轮询解决远程配置变化的实时监控Nacos使用spring-cloud-context的@RefreshScope和ContextRefresher.refresh实现配置热刷新参考:https://ctripcorp.github。io/apollo/#/zh/READMEhttps://blog.csdn.net/liuccc1/article/details/87002916https://blog.csdn.net/wangxindong11/article/details/78591396https://blog.csdn.net/u012410733/article/details/107119457https://www.cnblogs.com/javastack/p/12049139.html