Nacos或者Config是怎么实现配置热刷新的?
本文转载自微信公众号「Java大厂面试官」,置热作者laker。实刷新转载本文请联系Java大厂面试官公众号。现配 laker 前言 问题1. 如何实现配置热刷新 1. @RefreshScope原理 2. ContextRefresher.refresh() 3. RefreshScope.refreshAll() 4. 模拟造轮子 问题2. Nacos客户端如何实时监听到Nacos服务端配置更新了 1. Apollo 实现方式 2. 什么是置热DeferredResult 3. 模拟造轮子 总结 前言
文中大致介绍实现技术的关键点,以及如何模仿造个简易轮子(造轮子很重要,实刷新只有自己想着造轮子,现配才会问出很多原理问题),置热具体源码细节,实刷新请拿着文中的现配关键词自行google,然后跟着debug即可。置热
问题1. 如何实现配置热刷新重点 Nacos原理:
1.在需要热刷新的实刷新Bean上使用Spring Cloud原生注解 @RefreshScope 2.当有配置更新的时候调用contextRefresher.refresh()代码如下:
@RestController @RequestMapping("/config") @RefreshScope // 重点 public class ConfigController { @Value("${laker.name}") // 待刷新的属性 private String lakerName; @RequestMapping("/get") public String get() { return lakerName; } ... }1. @RefreshScope原理
@RefreshScope位于spring-cloud-context,源码注释如下:
可将@Bean定义放入org.springframework.cloud.context.scope.refresh.RefreshScope中。现配用这种方式注解的置热Bean可以在运行时刷新,并且使用它们的实刷新任何组件都将在下一个方法调用前获得一个新实例,该实例将完全初始化并注入所有依赖项。现配
要清楚RefreshScope,先要了解Scope
Scope(org.springframework.beans.factory.config.Scope)是Spring 2.0开始就有的核心的概念
RefreshScope(org.springframework.cloud.context.scope.refresh), 即@Scope("refresh")是spring cloud提供的一种特殊的scope实现,用来实现配置、实例热加载。
类似的有:
RequestScope:是服务器托管从当前web request中获取实例的实例 SessionScope:是从Session中获取实例的实例 ThreadScope:是从ThreadLocal中获取的实例RefreshScope是从内建缓存中获取的。
2. ContextRefresher.refresh()
当有配置更新的时候,触发ContextRefresher.refresh
RefreshScope 刷新过程
入口在ContextRefresher.refresh
public synchronized Set<String> refresh() { ① Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources()); ② updateEnvironment(); ④ Set<String> keys = changes(before, ③extract(this.context.getEnvironment().getPropertySources())).keySet(); ⑤ this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys)); ⑥ this.scope.refreshAll(); }①提取标准参数(SYSTEM,JNDI,SERVLET)之外所有参数变量
②把原来的Environment里的参数放到一个新建的Spring Context容器下重新加载,完事之后关闭新容器(重点:可以去debug跟踪下,实际上是重启了个SpringApplication)
③提起更新过的参数(排除标准参数)
④比较出变更项
⑤发布环境变更事件
⑥RefreshScope用新的环境参数重新生成Bean,重新生成的过程很简单,清除refreshscope缓存幷销毁Bean,下次就会重新从BeanFactory获取一个新的实例(该实例使用新的配置)
3. RefreshScope.refreshAll()
RefreshScope.refreshAll方法实现,即上面的第⑥步调用:
public void refreshAll() { super.destroy(); this.context.publishEvent(new RefreshScopeRefreshedEvent()); }RefreshScope类中有一个成员变量 cache,用于缓存所有已经生成的 Bean,在调用 get 方法时尝试从缓存加载,如果没有的话就生成一个新对象放入缓存,并通过 getBean 初始化其对应的亿华云计算 Bean:
public Object get(String name, ObjectFactory<?> objectFactory) { BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory)); this.locks.putIfAbsent(name, new ReentrantReadWriteLock()); try { return value.getBean(); } catch (RuntimeException e) { this.errors.put(name, e); throw e; } }所以在销毁时只需要将整个缓存清空,下次获取对象时自然就可以重新生成新的对象,也就自然绑定了新的属性:
public 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(); }清空缓存后,下次访问对象时就会重新创建新的对象并放入缓存了。
而在清空缓存后,它还会发出一个 RefreshScopeRefreshedEvent 事件,在某些 Spring Cloud 的组件中会监听这个事件并作出一些反馈。
4. 模拟造轮子
这里我们就可以模拟造个热更新的轮子了;
代码以及配置如下:
项目依赖spring-cloud-context <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-context</artifactId> </dependency> 配置bean @Component @RefreshScope public class User { @Value("${laker.name}") private String name; ... } 刷新接口以及查看接口 @RestController @RequestMapping("/config") public class ConfigController { @Autowired User user; @Autowired ContextRefresher contextRefresher; @RequestMapping("/get") public String get() { return user.getName(); } @RequestMapping("/refresh") public String[] refresh() { Set<String> keys = contextRefresher.refresh(); return keys.toArray(new String[keys.size()]); } application.yml laker: name: laker操作流程如下:
1.浏览器http://localhost:8080/config/get - 浏览器结果:laker
2.修改application.yml里面内容为:
laker: name: lakerupdate3.浏览器http://localhost:8080/config/refresh - 浏览器结果:laker.name
4.浏览器http://localhost:8080/config/get - 浏览器结果:lakerupdate(未重新启动,实现了配置更新)
问题2. Nacos客户端如何实时监听到Nacos服务端配置更新了
这里可以去看下Nacos源码,使用的是长轮询,什么是长轮询以及其其他替代协议?
RocketMQ Nacos Apollo Kafka自己花了几个小时去看Nacos长轮询源码,太多了不太好理解,有兴趣的自行google。一般我们都是基于Spring Boot的后台了,各种google后,发现Apollo实现较为简单,所以直接拿Apollo的代码借鉴。
1. Apollo 实现方式
实现方式如下:
客户端会发起一个Http请求到Config Service的notifications/v2接口,也就是NotificationControllerV2,b2b信息网参见RemoteConfigLongPollService NotificationControllerV2不会立即返回结果,而是通过Spring DeferredResult把请求挂起 如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端 如果有该客户端关心的配置发布,NotificationControllerV2会调用DeferredResult的setResult方法,传入有配置变化的namespace信息,同时该请求会立即返回。客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service获取该namespace的最新配置。解读下:
关键词DeferredResult,使用这个特性来实现长轮询 超时返回的时候,是返回的状态码Http Code 304释义:自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容,进而节省带宽和开销。
2. 什么是DeferredResult
异步支持是在Servlet 3.0中引入的,简单来说,它允许在请求接收器线程之外的另一个线程中处理HTTP请求。
从Spring 3.2开始可用的DeferredResult有助于将长时间运行的计算从http-worker线程卸载到单独的线程。
尽管另一个线程将占用一些资源来进行计算,但不会阻止工作线程,并且可以处理传入的客户端请求。
异步请求处理模型非常有用,因为它有助于在高负载期间很好地扩展应用程序,尤其是对于IO密集型操作。
DeferredResult是对异步Servlet的封装
具体可以参考我在CSDN写的Spring Boot 使用DeferredResult实现长轮询
这里借助互联网上的一个图就更清晰些。
Servlet异步流程图

接收到request请求之后,由tomcat工作线程从HttpServletRequest中获得一个异步上下文AsyncContext对象,然后由tomcat工作线程把AsyncContext对象传递给业务处理线程,同时tomcat工作线程归还到工作线程池,这一步就是异步开始。在业务处理线程中完成业务逻辑的处理,生成response返回给客户端。
3. 模拟造轮子
这里我们通过使用 Spring Boot 来简单的模拟一下如何通过 Spring Boot DeferredResult 来实现长轮询服务推送的。
代码如下,仅供参考:
/** * 模拟Config Service通知客户端的长轮询实现原理 */ @RestController @RequestMapping("/config") public class LakerConfigController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); //guava中的Multimap,多值map,对map的增强,一个key可以保持多个value private Multimap<String, DeferredResult<String>> watchRequests = Multimaps.synchronizedSetMultimap(HashMultimap.create()); /** * 模拟长轮询 */ @RequestMapping(value = "/get/{dataId}") public DeferredResult<String> watch(@PathVariable("dataId") String dataId) { logger.info("Request received"); ResponseEntity<String> NOT_MODIFIED_RESPONSE = new ResponseEntity<>(HttpStatus.NOT_MODIFIED); // 超时时间30s 返回 304 状态码告诉客户端当前命名空间的配置文件并没有更新 DeferredResult<String> deferredResult = new DeferredResult<>(30 * 1000L, NOT_MODIFIED_RESPONSE); //当deferredResult完成时(不论是超时还是异常还是正常完成),移除watchRequests中相应的watch key deferredResult.onCompletion(() -> { logger.info("remove key:" + dataId); watchRequests.remove(dataId, deferredResult); }); deferredResult.onTimeout(() -> { logger.info("onTimeout()"); }); watchRequests.put(dataId, deferredResult); logger.info("Servlet thread released"); return deferredResult; } /** * 模拟发布配置 */ @RequestMapping(value = "/update/{dataId}") public Object publishConfig(@PathVariable("dataId") String dataId) { if (watchRequests.containsKey(dataId)) { Collection<DeferredResult<String>> deferredResults = watchRequests.get(dataId); Long time = System.currentTimeMillis(); //通知所有watch这个namespace变更的长轮训配置变更结果 for (DeferredResult<String> deferredResult : deferredResults) { //deferredResult一旦执行了setResult()方法,就说明DeferredResult正常完成了,会立即把结果返回给客户端 deferredResult.setResult(dataId + " changed:" + time); } } return "success"; } }操作流程如下:
为了简便我用浏览器模拟,实际用Java Http Client,例如:okhttp、Apache http client等
正常流程:
client1浏览器http://localhost:8080/config/get/laker,阻塞中ing client2浏览器http://localhost:8080/config/update/laker,返回success client1浏览器http://localhost:8080/config/get/laker,返回laker changed:1611022736865超时流程:
client1浏览器http://localhost:8080/config/get/laker,阻塞中ing 30s后 client1浏览器,返回http code 304
在这里插入图片描述
总结
Nacos使用长轮询解决了实时监听远端配置变更 Nacos使用spring-cloud-context的@RefreshScope和ContextRefresher.refresh实现了配置热刷新参考:
https://ctripcorp.github.io/apollo/#/zh/README https://blog.csdn.net/liuccc1/article/details/87002916 https://blog.csdn.net/wangxindong11/article/details/78591396 https://blog.csdn.net/u012410733/article/details/107119457 https://www.cnblogs.com/javastack/p/12049139.html相关文章
Ubuntu Kylin 14.10是一个新版本的具有中国特色的操作系统。新版本的操作系统新增了很多人性化的设计,小编为大家介绍一些相关的功能给各位对于Ubuntu Kylin不是很明白的地方。1、如2025-11-05
.com域名是国际最广泛流行的通用域名,目前全球注册量第一的域名,公司企业注册域名的首选。国际化公司通常会注册该类域名。
小白怎么区别域名cn和com的?有什么不同?域名cn属于国家域名,域名com属于通用顶级域名,这是两者最大的区别。此外,还有注册价格、域名注册等方面的差异。下面益华网络就带大家看看小白怎么区别域名cn2025-11-05付款完成后,您只需耐心等待,如果您注册成功,系统会提示您。这里需要注意的是,域名是一个即时产品,只有在最终付款成功时才能预订,注册成功后不能更改。
如何知道公司购买域名?有什么需要注意的?众所周知,一个公司成立的时候,需要做很多事情,这些准备和所有的努力都是为了为未来公司的成长打下基础,所以这段时间做的每一个决定都不能马虎,而公司需要准备在注册之2025-11-05
解析之后一般在十分钟内生效,如果没有生效可以联系域名服务商进行沟通。
域名该怎么才能解析?新手解析如何做?域名解析是把域名指向网站空间IP,让人们通过注册的域名可以方便地访问到网站一种服务。您可以简单地理解成域名解析就是告诉互联网您希望用您的域名访问哪个空间地址。下面益2025-11-05Office降价,提供高性价比办公解决方案(办公软件Office大幅降价,为企业带来实惠与高效)
摘要:办公软件对于现代企业来说是必不可少的工具,而微软的Office套件一直以来都是企业首选。近日,微软宣布将对Office套件进行大幅度的降价,以提供更加实惠和高效的办公解决方案。本文...2025-11-05
投资各类域名就像到处打游击战,结果处处失败。因为这样,对任何一个中国域名市场的走势和价格都没有准确的把握,所以最好缩小范围,准确把握战场态势,埋伏。
成熟的域名投资需要做到哪几点?新手必学的投资方法;现在它的实现域名的投资者往往是疯狂的,所以建议大家冷静下来,给大家一些有用的域名建议。我们投资域名的时候,一定不能有投机的心。我相信大家都能理解,如果2025-11-05

最新评论