文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Nacos或者Config是怎么实现配置热刷新的?

2024-12-03 12:41

关注

文中大致介绍实现技术的关键点,以及如何模仿造个简易轮子(造轮子很重要,只有自己想着造轮子,才会问出很多原理问题),具体源码细节,请拿着文中的关键词自行google,然后跟着debug即可。

问题1. 如何实现配置热刷新重点 Nacos原理:

代码如下:

  1. @RestController 
  2. @RequestMapping("/config"
  3. @RefreshScope // 重点 
  4. public class ConfigController { 
  5.     @Value("${laker.name}") // 待刷新的属性 
  6.     private String lakerName; 
  7.     @RequestMapping("/get"
  8.     public String get() { 
  9.         return lakerName; 
  10.     } 
  11.  ... 

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实现,用来实现配置、实例热加载。

类似的有:

RefreshScope是从内建缓存中获取的。

2. ContextRefresher.refresh()

当有配置更新的时候,触发ContextRefresher.refresh

RefreshScope 刷新过程

入口在ContextRefresher.refresh

  1. public synchronized Set refresh() { 
  2. ①  Map before = extract(this.context.getEnvironment().getPropertySources()); 
  3. ②  updateEnvironment(); 
  4. ④  Set keys = changes(before, ③extract(this.context.getEnvironment().getPropertySources())).keySet(); 
  5. ⑤  this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys)); 
  6. ⑥       this.scope.refreshAll(); 

①提取标准参数(SYSTEM,JNDI,SERVLET)之外所有参数变量

②把原来的Environment里的参数放到一个新建的Spring Context容器下重新加载,完事之后关闭新容器(重点:可以去debug跟踪下,实际上是重启了个SpringApplication)

③提起更新过的参数(排除标准参数)

④比较出变更项

⑤发布环境变更事件

⑥RefreshScope用新的环境参数重新生成Bean,重新生成的过程很简单,清除refreshscope缓存幷销毁Bean,下次就会重新从BeanFactory获取一个新的实例(该实例使用新的配置)

3. RefreshScope.refreshAll()

RefreshScope.refreshAll方法实现,即上面的第⑥步调用:

  1. public void refreshAll() { 
  2.   super.destroy(); 
  3.   this.context.publishEvent(new RefreshScopeRefreshedEvent()); 

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

  1. public Object get(String name, ObjectFactory objectFactory) { 
  2.  BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory)); 
  3.  this.locks.putIfAbsent(name, new ReentrantReadWriteLock()); 
  4.  try { 
  5.   return value.getBean(); 
  6.  } 
  7.  catch (RuntimeException e) { 
  8.   this.errors.put(name, e); 
  9.   throw e; 
  10.  } 

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

  1. public void destroy() { 
  2.  List errors = new ArrayList(); 
  3.  Collection wrappers = this.cache.clear(); 
  4.  for (BeanLifecycleWrapper wrapper : wrappers) { 
  5.   try { 
  6.    Lock lock = this.locks.get(wrapper.getName()).writeLock(); 
  7.    lock.lock(); 
  8.    try { 
  9.     wrapper.destroy(); 
  10.    } 
  11.    finally { 
  12.     lock.unlock(); 
  13.    } 
  14.   } 
  15.   catch (RuntimeException e) { 
  16.    errors.add(e); 
  17.   } 
  18.  } 
  19.  if (!errors.isEmpty()) { 
  20.   throw wrapIfNecessary(errors.get(0)); 
  21.  } 
  22.  this.errors.clear(); 

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

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

4. 模拟造轮子

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

代码以及配置如下:

  1.  
  2.   org.springframework.cloud 
  3.   spring-cloud-context 
  4.  
  1. @Component 
  2. @RefreshScope 
  3. public class User { 
  4.     @Value("${laker.name}"
  5.     private String name
  6.     ... 
  1. @RestController 
  2. @RequestMapping("/config"
  3. public class ConfigController { 
  4.     @Autowired 
  5.     User user
  6.     @Autowired 
  7.     ContextRefresher contextRefresher; 
  8.     @RequestMapping("/get"
  9.     public String get() { 
  10.         return user.getName(); 
  11.     } 
  12.     @RequestMapping("/refresh"
  13.     public String[] refresh() { 
  14.         Set keys = contextRefresher.refresh(); 
  15.         return keys.toArray(new String[keys.size()]); 
  16.     } 
  1. laker: 
  2.   name: laker 

操作流程如下:

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

修改application.yml里面内容为:

  1. laker: 
  2.   name: lakerupdate 

浏览器http://localhost:8080/config/refresh - 浏览器结果:laker.name

浏览器http://localhost:8080/config/get - 浏览器结果:lakerupdate(未重新启动,实现了配置更新)

问题2. Nacos客户端如何实时监听到Nacos服务端配置更新了

这里可以去看下Nacos源码,使用的是长轮询,什么是长轮询以及其其他替代协议?

自己花了几个小时去看Nacos长轮询源码,太多了不太好理解,有兴趣的自行google。一般我们都是基于Spring Boot的后台了,各种google后,发现Apollo实现较为简单,所以直接拿Apollo的代码借鉴。

1. Apollo 实现方式

实现方式如下:

  1. 客户端会发起一个Http请求到Config Service的notifications/v2接口,也就是NotificationControllerV2,参见RemoteConfigLongPollService
  2. NotificationControllerV2不会立即返回结果,而是通过Spring DeferredResult把请求挂起
  3. 如果在60秒内没有该客户端关心的配置发布,那么会返回Http状态码304给客户端
  4. 如果有该客户端关心的配置发布,NotificationControllerV2会调用DeferredResult的setResult方法,传入有配置变化的namespace信息,同时该请求会立即返回。客户端从返回的结果中获取到配置变化的namespace后,会立即请求Config Service获取该namespace的最新配置。

解读下:

释义:自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容,进而节省带宽和开销。

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 来实现长轮询服务推送的。

代码如下,仅供参考:

  1.  
  2. @RestController 
  3. @RequestMapping("/config"
  4. public class LakerConfigController { 
  5.     private final Logger logger = LoggerFactory.getLogger(this.getClass()); 
  6.     //guava中的Multimap,多值map,对map的增强,一个key可以保持多个value 
  7.     private Multimap> watchRequests = Multimaps.synchronizedSetMultimap(HashMultimap.create()); 
  8.      
  9.     @RequestMapping(value = "/get/{dataId}"
  10.     public DeferredResult watch(@PathVariable("dataId") String dataId) { 
  11.         logger.info("Request received"); 
  12.         ResponseEntity 
  13.                 NOT_MODIFIED_RESPONSE = new ResponseEntity<>(HttpStatus.NOT_MODIFIED); 
  14.         // 超时时间30s 返回 304 状态码告诉客户端当前命名空间的配置文件并没有更新 
  15.         DeferredResult deferredResult = new DeferredResult<>(30 * 1000L, NOT_MODIFIED_RESPONSE); 
  16.         //当deferredResult完成时(不论是超时还是异常还是正常完成),移除watchRequests中相应的watch key 
  17.         deferredResult.onCompletion(() -> { 
  18.             logger.info("remove key:" + dataId); 
  19.             watchRequests.remove(dataId, deferredResult); 
  20.         }); 
  21.         deferredResult.onTimeout(() -> { 
  22.             logger.info("onTimeout()"); 
  23.         }); 
  24.         watchRequests.put(dataId, deferredResult); 
  25.         logger.info("Servlet thread released"); 
  26.         return deferredResult; 
  27.     } 
  28.      
  29.     @RequestMapping(value = "/update/{dataId}"
  30.     public Object publishConfig(@PathVariable("dataId") String dataId) { 
  31.         if (watchRequests.containsKey(dataId)) { 
  32.             Collection> deferredResults = watchRequests.get(dataId); 
  33.             Long time = System.currentTimeMillis(); 
  34.             //通知所有watch这个namespace变更的长轮训配置变更结果 
  35.             for (DeferredResult deferredResult : deferredResults) { 
  36.                 //deferredResult一旦执行了setResult()方法,就说明DeferredResult正常完成了,会立即把结果返回给客户端 
  37.                 deferredResult.setResult(dataId + " changed:" + time); 
  38.             } 
  39.         } 
  40.         return "success"
  41.     } 

操作流程如下:

为了简便我用浏览器模拟,实际用Java Http Client,例如:okhttp、Apache http client等

正常流程:

超时流程:

在这里插入图片描述

 

总结

参考:

 

 

来源:Java大厂面试官内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯