文本已收录至个人GitHub仓库,欢迎Star:github.com/bin39232820…
种一棵树最好的时间是十年前,其次是如今
我知道不少人不玩qq了,可是怀旧一下,欢迎加入六脉神剑Java菜鸟学习群,群聊号码:549684836 鼓励你们在技术的路上写博客java
上篇文章和你们分析了 Nacos 的配置中心原理,主要分析了 Nacos 客户端是如何感知到服务端的配置变动的,可是只是从客户端的角度进行了分析,并无从服务端的角度进行分析,本篇文章我将结合服务端从两个角度来分析配置变动是如何通知到客户端的。git
从上篇文章中咱们已经知道了 Nacos 的客户端维护了一个长轮询的任务,去检查服务端的配置信息是否发生变动,若是发生了变动,那么客户端会拿到变动的 groupKey 再根据 groupKey 去获取配置项的最新值便可。github
每次都靠客户端去发请求,询问服务端我所关注的配置项有没有发生变动,那请求的间隔改设置为多少才合适呢?bash
若是间隔时间设置的太长的话有可能没法及时获取服务端的变动,若是间隔时间设置的过短的话,那么频繁的请求对于服务端来讲无疑也是一种负担。分布式
因此最好的方式是客户端每隔一段长度适中的时间去服务端请求,而在这期间若是配置发生变动,服务端可以主动将变动后的结果推送给客户端,这样既能保证客户端可以实时感知到配置的变化,也下降了服务端的压力。post
如今让咱们再次回到客户端长轮询的部分,也就是 LongPollingRunnable 中的 checkUpdateDataIds 方法,该方法就是用来访问服务端的配置是否发生变动的,该方法最终会调用以下图所示的方法:学习
请注意图中红框部分的内容,客户端是经过一个 http 的 post 请求去获取服务端的结果的,而且设置了一个超时时间:30s。url
这个信息很关键,为何客户端要等待 30s 才超时呢?不该该越快获得结果越好吗,咱们来验证下该方法是否是真的等待了 30s。spa
在 LongPollingRunnable 中的 checkUpdateDataIds 方法先后加上时间计算,而后将所消耗的时间打印出来,以下图所示:3d
而后咱们启动客户端,观察打印的日志,以下图所示:
从打印出来的日志能够看出来,客户端足足等了29.5+s,才请求到服务端的结果。而后客户端获得服务端的结果以后,再作一些后续的操做,所有都执行完毕以后,在 finally 中又从新调用了自身,也就是说这个过程是一直循环下去的。
如今咱们能够肯定的是,客户端向服务端发起一次请求,最少要29.5s才能获得结果,固然啦,这是在配置没有发生变化的状况下。
若是客户端在长轮询时配置发生变动的话,该请求须要多长时间才会返回呢,咱们继续作一个实验,在客户端长轮询时修改配置,结果以下图所示:
上图中红框中就是我在客户端一发起请求时就更新配置后打印的结果,从结果能够看出来该请求并无等到 29.5s+ 才返回,而是一个很短的时间就返回了,具体多久须要从服务端的实现中查询答案。
到目前为止咱们已经知道了客户端执行长轮询的逻辑,以及每次请求的响应时间会随着服务端配置是否变动而发生变化,具体能够用下图描述:
分析完客户端的状况,接下来要重点分析服务端是如何实现的,而且要带着几个问题去寻找答案:
首先咱们从客户端发送的 http 请求中能够知道,请求的是服务端的 /v1/cs/configs/listener 这个接口。
能够看到该方法是一个轮询的接口,除了支持长轮询外还支持短轮询的逻辑,这里咱们只关心长轮训
再次进入 longPollingService 的 addLongPollingClient 方法,以下图所示:
从该方法的名字咱们能够知道,该方法主要是将客户端的长轮询请求添加到某个东西中去,在方法的最后一行咱们获得了答案:服务端将客户端的长轮询请求封装成一个叫 ClientLongPolling 的任务,交给 scheduler 去执行。
可是请注意我用红框圈出来的代码,服务端拿到客户端提交的超时时间后,又减去了 500ms 也就是说服务端在这里使用了一个比客户端提交的时间少 500ms 的超时时间,也就是 29.5s,看到这个 29.5s 咱们应该有点兴奋了。
PS:这里的 timeout 不必定一直是 29.5,当 isFixedPolling() 方法为 true 时,timeout 将会是一个固定的间隔时间,这里为了描述简单就直接用 29.5 来进行说明。
接下来咱们来看服务端封装的 ClientLongPolling 的任务到底执行的什么操做,以下图所示:
com.alibaba.nacos.config.server.service.LongPollingService.ClientLongPolling.java
复制代码
ClientLongPolling 被提交给 scheduler 执行以后,实际执行的内容能够拆分红如下四个步骤:
整个过程能够用下面的图进行描述:
这里出现了一个很关键的 allSubs 对象,该对象是一个 ConcurrentLinkedQueue 队列,ClientLongPolling 将自身添加到队列中去确定是有缘由的,这里须要对 allSubs 留个心眼。
咱们先无论 allSubs 队列具体作了什么事,先来看下服务端过了 29.5s 的延时时间后,执行调度任务时作了什么,也就是上图中对应的第3、第四步。
首先将自身从 allSubs 队列中删除掉,也就是如注释中说的:删除订阅关系,从这里咱们能够知道 allSubs 和 ClientLongPolling 之间维持了一种订阅关系,而 ClientLongPolling 是被订阅的。
PS:删除掉订阅关系以后,订阅方就没法对被订阅方进行通知了。
而后服务端对客户端提交上来的 groupKey 进行检查,若是发现某一个 groupKey 的 md5 值还不是最新的,则说明客户端的配置项还没发生变动,因此将该 groupKey 放到一个 changedGroupKeys 列表中,最后将该 changedGroupKeys 返回给客户端。
对于客户端来讲,只要拿到 changedGroupKeys 便可,后续的操做我在上一篇文章中已经分析过了。
服务端直到调度任务的延时时间到了以前,ClientLongPolling 都不会有其余的任务可作,因此在这段时间内,该 allSubs 队列确定有事情须要进行处理。
回想到咱们在客户端长轮询期间,更改了配置以后,客户端可以当即获得响应,因此咱们有理由相信,这个队列可能会跟配置变动有关系。
如今咱们找一下在 dashboard 上修改配置后,调用的请求,能够很容易的找到该请求对应的 url为:/v1/cs/configs 而且是一个 POST 请求,具体的方法是 ConfigController 中的 publishConfig 方法,以下图所示:
我只截取了重要的部分,从红框中的代码能够看出,修改配置后,服务端首先将配置的值进行了持久化层的更新,而后触发了一个 ConfigDataChangeEvent 的事件。
具体的 fireEvent 的方法以下图所示:
com.alibaba.nacos.config.server.utils.event.EventDispatcher.java
复制代码
fireEvent 方法其实是触发的 AbstractEventListener 的 onEvent 方法,而全部的 listener 是保存在一个叫 listeners 对象中的。
被触发的 AbstractEventListener 对象则是经过 addEventListener 方法添加到 listeners 中的,因此咱们只须要找到 addEventListener 方法在何处被调用的,就知道有哪些 AbstractEventListener 须要被触发 onEvent 回调方法了。
能够找到是在 AbstractEventListener 类的构造方法中,将自身注册进去了,以下图所示:
com.alibaba.nacos.config.server.utils.event.EventDispatcher.AbstractEventListener.java
复制代码
而 AbstractEventListener 是一个抽象类,因此实际注册的应该是 AbstractEventListener 的子类,因此咱们须要找到因此继承自 AbstractEventListener 的类,以下图所示:
能够看到 AbstractEventListener 全部的子类中,有一个咱们熟悉的身影,他就是咱们刚刚一直在研究的 LongPollingService。
因此到这里咱们就知道了,当咱们从 dashboard 中更新了配置项以后,实际会调用到 LongPollingService 的 onEvent 方法。
如今咱们继续回到 LongPollingService 中,查看一下 onEvent 方法,以下图所示:
com.alibaba.nacos.config.server.service.LongPollingService.DataChangeTask.java
复制代码
发现当触发了 LongPollingService 的 onEvent 方法时,实际是执行了一个叫 DataChangeTask 的任务,应该是经过该任务来通知客户端服务端的数据已经发生了变动,咱们进入 DataChangeTask 中看下具体的代码,以下图所示:
代码很简单,能够总结为两个步骤:
首先遍历 allSubs 的队列,该队列中维持的是全部客户端的请求任务,须要找到与当前发生变动的配置项的 groupKey 相等的 ClientLongPolling 任务
在第一步找到具体的 ClientLongPolling 任务后,只须要将发生变动的 groupKey 经过该 ClientLongPolling 写入到响应对象中,就完成了一次数据变动的 “推送” 操做了 若是 DataChangeTask 任务完成了数据的 “推送” 以后,ClientLongPolling 中的调度任务又开始执行了怎么办呢? 很简单,只要在进行 “推送” 操做以前,先将原来等待执行的调度任务取消掉就能够了,这样就防止了推送操做写完响应数据以后,调度任务又去写响应数据,这时确定会报错的。 能够从 sendResponse 方法中看到,确实是这样作的:
基于上述的分析,最终总结了如下结论:
服务端的更新咱们看完了,下次还得学习一下客户端是怎么和Spring结合让配置刷新的
好了各位,以上就是这篇文章的所有内容了,能看到这里的人呀,都是真粉。
创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见
六脉神剑 | 文 【原创】若是本篇博客有任何错误,请批评指教,不胜感激 !