服务器的config如何修改,Nacos或者Config是怎么实现配置热刷新的?

b4377a8a85dd2601d95d281224a54a35.png

本文转载自微信公众号「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 // 重点

publicclass ConfigController {

@Value("${laker.name}") // 待刷新的属性

private String lakerName;

@RequestMapping("/get")

publicString get() {

returnlakerName;

}

...

}

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

publicsynchronizedSet refresh() {

①  Map before = extract(this.context.getEnvironment().getPropertySources());

②  updateEnvironment();

④  Set 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方法实现,即上面的第⑥步调用:

publicvoid refreshAll() {

super.destroy();

this.context.publishEvent(new RefreshScopeRefreshedEvent());

}

RefreshScope类中有一个成员变量 cache,用于缓存所有已经生成的 Bean,在调用 get 方法时尝试从缓存加载,如果没有的话就生成一个新对象放入缓存,并通过 getBean 初始化其对应的 Bean:

publicObject get(Stringname, ObjectFactory> objectFactory) {

BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));

this.locks.putIfAbsent(name, new ReentrantReadWriteLock());

try {

returnvalue.getBean();

}

catch (RuntimeException e) {

this.errors.put(name, e);

throw e;

}

}

所以在销毁时只需要将整个缓存清空,下次获取对象时自然就可以重新生成新的对象,也就自然绑定了新的属性:

publicvoid 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();

}

清空缓存后,下次访问对象时就会重新创建新的对象并放入缓存了。

而在清空缓存后,它还会发出一个 RefreshScopeRefreshedEvent 事件,在某些 Spring Cloud 的组件中会监听这个事件并作出一些反馈。

4. 模拟造轮子

这里我们就可以模拟造个热更新的轮子了;

代码以及配置如下:

项目依赖spring-cloud-context

org.springframework.cloud

spring-cloud-context

配置bean

@Component

@RefreshScope

publicclassUser{

@Value("${laker.name}")

private String name;

...

}

刷新接口以及查看接口

@RestController

@RequestMapping("/config")

publicclass ConfigController {

@Autowired

Useruser;

@Autowired

ContextRefresher contextRefresher;

@RequestMapping("/get")

publicString get() {

returnuser.getName();

}

@RequestMapping("/refresh")

publicString[] refresh() {

Set keys = contextRefresher.refresh();

returnkeys.toArray(new String[keys.size()]);

}

application.yml

laker:

name: laker

操作流程如下:

1.浏览器http://localhost:8080/config/get - 浏览器结果:laker

2.修改application.yml里面内容为:

laker:

name: lakerupdate

3.浏览器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,参见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异步流程图

d88b195312061605a55254db0949fb31.png

接收到request请求之后,由tomcat工作线程从HttpServletRequest中获得一个异步上下文AsyncContext对象,然后由tomcat工作线程把AsyncContext对象传递给业务处理线程,同时tomcat工作线程归还到工作线程池,这一步就是异步开始。在业务处理线程中完成业务逻辑的处理,生成response返回给客户端。

3. 模拟造轮子

这里我们通过使用 Spring Boot 来简单的模拟一下如何通过 Spring Boot DeferredResult 来实现长轮询服务推送的。

代码如下,仅供参考:

/**

* 模拟Config Service通知客户端的长轮询实现原理

*/

@RestController

@RequestMapping("/config")

publicclass LakerConfigController {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

//guava中的Multimap,多值map,对map的增强,一个key可以保持多个value

private Multimap> watchRequests = Multimaps.synchronizedSetMultimap(HashMultimap.create());

/**

* 模拟长轮询

*/

@RequestMapping(value = "/get/{dataId}")

publicDeferredResult watch(@PathVariable("dataId") String dataId) {

logger.info("Request received");

ResponseEntity

NOT_MODIFIED_RESPONSE = new ResponseEntity<>(HttpStatus.NOT_MODIFIED);

// 超时时间30s 返回 304 状态码告诉客户端当前命名空间的配置文件并没有更新

DeferredResult 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");

returndeferredResult;

}

/**

* 模拟发布配置

*/

@RequestMapping(value = "/update/{dataId}")

publicObject publishConfig(@PathVariable("dataId") String dataId) {

if (watchRequests.containsKey(dataId)) {

Collection> deferredResults = watchRequests.get(dataId);

Long time= System.currentTimeMillis();

//通知所有watch这个namespace变更的长轮训配置变更结果

for(DeferredResult 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

104634351b4c6d7ab3cda10f47d8825d.png

在这里插入图片描述

总结

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

【编辑推荐】

【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值