既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
💖前言
这篇文章就介绍下,服务发现的入口是什么?本地缓存数据结构、缓存时机、如果缓存中没有如何处理?使用了定时任务,那定时任务的底层基于什么的、它是干什么的、定时间隔?监听服务端UDP通知、发送ACK?发送http请求到服务端,谁发的、如何接收?服务端如何推送服务实例的、采用什么方式?带着这些问题,下面我们来探究探究。
注意:Nacos源码版本为1.x
💖Nacos服务发现
✨流程图
✨服务发现的入口
💫SpringCloud原生项目spring-cloud-commons
你会发现@EnableDiscoveryClient注解也是在spring-cloud-commons项目,还有个discovery文件夹。我们本节注意下DiscoveryClient接口,以及其中声明的接口方法。SpringCloud是由几个关键项目组成的,spring-cloud-commons项目是其中之一。SpringCloud Alibaba也不是完全替代SpringCloud的,一些基本的规范还是继承下来了,做扩展等。
💫Nacos是如何继承下来的?
Nacos是通过自己的spring-cloud-alibaba-nacos-discovery项目去集成到SpringCloud的以及基于SpringBoot的自动装配机制集成到SpringBoot项目的。而服务发现方面,NacosDiscoveryClient 实现了spring-cloud-commons项目的DiscoveryClient接口,即Nacos中服务发现入口是NacosDiscoveryClient类。
点击方法继续跟进到下面的逻辑
💫NacosServiceDiscovery#getInstances()获取服务实例
public List<ServiceInstance> getInstances(String serviceId) throws NacosException {
// 获取配置文件组信息
String group = this.discoveryProperties.getGroup();
// 调用API模块中NamingService的selectInstances()方法,
// 引用是NacosNamingService的反射获取,之前文章已分析
List<Instance> instances = this.namingService().selectInstances(serviceId, group, true);
// 将Nacos的服务实例适配为SpringCloud的ServiceInstance服务实例
return hostToServiceInstanceList(instances, serviceId);
}
主要逻辑:
- 获取配置文件组信息
- 调用API模块中NamingService接口的selectInstances()方法。引用是NacosNamingService的,通过反射获取,之前文章已详细分析。NacosNamingService是Nacos的client模块里面的一个组件,下面分析。
- 将Nacos的服务实例适配为SpringCloud的ServiceInstance服务实例
✨NacosNamingService初始化流程
它的构造方法是在NamingFactory通过反射方式调用的,上面也提到了。因为这个流程也是不小的,故在获取服务实例前先讲解。
💖NacosNamingService构造初始化
public NacosNamingService(Properties properties) throws NacosException {
init(properties);
}
private void init(Properties properties) throws NacosException {
ValidatorUtils.checkInitParam(properties);
this.namespace = InitUtils.initNamespaceForNaming(properties);
InitUtils.initSerialization();
initServerAddr(properties);
InitUtils.initWebRootContext();
initCacheDir();
initLogName(properties);
this.eventDispatcher = new EventDispatcher();
// 初始化服务代理
this.serverProxy = new NamingProxy(this.namespace, this.endpoint, this.serverList, properties);
// 初始化心跳组件
this.beatReactor = new BeatReactor(this.serverProxy, initClientBeatThreadCount(properties));
// 初始化hostReactor
this.hostReactor = new HostReactor(this.eventDispatcher, this.serverProxy, beatReactor, this.cacheDir,
isLoadCacheAtStart(properties), initPollingThreadCount(properties));
}
初始化服务代理、心跳发送组件以及hostReactor,重点看hostReactor的构造初始化
💫HostReactor构造初始化
public HostReactor(EventDispatcher eventDispatcher, NamingProxy serverProxy, BeatReactor beatReactor,
String cacheDir, boolean loadCacheAtStart, int pollingThreadCount) {
// init executorService
this.executor = new ScheduledThreadPoolExecutor(pollingThreadCount, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("com.alibaba.nacos.client.naming.updater");
return thread;
}
});
this.eventDispatcher = eventDispatcher;
this.beatReactor = beatReactor;
this.serverProxy = serverProxy;
this.cacheDir = cacheDir;
// 初始化本地缓存
if (loadCacheAtStart) {
this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>(DiskCache.read(this.cacheDir));
} else {
this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>(16);
}
this.updatingMap = new ConcurrentHashMap<String, Object>();
this.failoverReactor = new FailoverReactor(this, cacheDir);
// 初始化pushReceiver
this.pushReceiver = new PushReceiver(this);
}
初始化本地缓存、pushReceiver,重点关注PushReceiver的构造方法
💖PushReceiver构造初始化
public PushReceiver(HostReactor hostReactor) {
try {
this.hostReactor = hostReactor;
// 初始化udp套接字
this.udpSocket = new DatagramSocket();
// 启动一个线程
this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("com.alibaba.nacos.naming.push.receiver");
return thread;
}
});
// 执行任务,下面的run()
this.executorService.execute(this);
} catch (Exception e) {
NAMING_LOGGER.error("[NA] init udp socket failed", e);
}
}
初始化udp套接字用于监听注册中心变更服务推送以及发送ack确认、启动一个线程死循环用于监听注册中心udp推送服务变更、执行任务,this就是PushReceiver的引用即任务,所以执行下面的run()逻辑。
💫PushReceiver#run
@Override
public void run() {
while (!closed) {
try {
// byte[] is initialized with 0 full filled by default
byte[] buffer = new byte[UDP_MSS];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// 监听Nacos服务端服务实例信息变更后的通知
udpSocket.receive(packet);
String json = new String(IoUtils.tryDecompress(packet.getData()), UTF_8).trim();
NAMING_LOGGER.info("received push data: " + json + " from " + packet.getAddress().toString());
PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class);
String ack;
if ("dom".equals(pushPacket.type) || "service".equals(pushPacket.type)) {
// 将数据缓存到本地
hostReactor.processServiceJson(pushPacket.data);
// send ack to server
ack = "{\"type\": \"push-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime + "\", \"data\":"
+ "\"\"}";
} else if ("dump".equals(pushPacket.type)) {
// dump data to server
ack = "{\"type\": \"dump-ack\"" + ", \"lastRefTime\": \"" + pushPacket.lastRefTime + "\", \"data\":"
+ "\"" + StringUtils.escapeJavaScript(JacksonUtils.toJson(hostReactor.getServiceInfoMap()))
+ "\"}";
} else {
// do nothing send ack only
ack = "{\"type\": \"unknown-ack\"" + ", \"lastRefTime\":\"" + pushPacket.lastRefTime
+ "\", \"data\":" + "\"\"}";
}
// 发送ack到服务端
udpSocket.send(new DatagramPacket(ack.getBytes(UTF_8), ack.getBytes(UTF_8).length,
packet.getSocketAddress()));
} catch (Exception e) {
NAMING_LOGGER.error("[NA] error while receiving push data", e);
}
}
}
主要逻辑:
- 监听Nacos服务端服务实例信息变更后的通知
- 解析注册中心推送的结果,组装回调ack报文,将注册中心推送的变更服务信息缓存到本地
- 发送ack到注册中心,以便注册中心决定是否需要重试。
✨从集成的client模块本地服务发现
本节点讲解的就是客户端服务发现,之所以这样说是因为SpringBoot的自动装配将Nacos的client模块集成进来了,想了解更多去看前面的文章分析。
💫获取服务实例列表
调用重载的selectInstances()方法,healthy默认true即健康,subscribe默认true即订阅
@Override
public List<Instance> selectInstances(String serviceName, String groupName, List<String> clusters, boolean healthy,
boolean subscribe) throws NacosException {
ServiceInfo serviceInfo;
// 默认订阅模式
if (subscribe) {
// 委托hostReactor处理
serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
} else {
serviceInfo = hostReactor
.getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName),
StringUtils.join(clusters, ","));
}
// 选取健康实例
return selectInstances(serviceInfo, healthy);
}
默认使用订阅模式,但是委托hostReactor去获取服务信息,以服务名、分组拼接作为入参即Nacos可识别的服务名。
💖从本地缓存/发送http从服务端获取服务信息
public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
// failover-mode:默认false
NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
String key = ServiceInfo.getKey(serviceName, clusters);
if (failoverReactor.isFailoverSwitch()) {
return failoverReactor.getService(key);
}
// 从本地缓存serviceInfoMap获取
ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
// 如果本地缓存中没有,则发送HTTP调用从Nacos服务端获取
if (null == serviceObj) {
serviceObj = new ServiceInfo(serviceName, clusters);
serviceInfoMap.put(serviceObj.getKey(), serviceObj);
updatingMap.put(serviceName, new Object());
// 更新服务
updateServiceNow(serviceName, clusters);
updatingMap.remove(serviceName);
} else if (updatingMap.containsKey(serviceName)) {
if (UPDATE_HOLD_INTERVAL > 0) {
// hold a moment waiting for update finish等待更新完成
synchronized (serviceObj) {
try {
serviceObj.wait(UPDATE_HOLD_INTERVAL);
} catch (InterruptedException e) {
NAMING_LOGGER
.error("[getServiceInfo] serviceName:" + serviceName + ", clusters:" + clusters, e);
}
}
}
}
// 开启一个定时任务,每隔1秒从Nacos服务端获取最新的服务实例信息,
// 更新到本地缓存seriveInfoMap中
scheduleUpdateIfAbsent(serviceName, clusters);
// 从本地缓存serviceInfoMap中获取服务实例信息
return serviceInfoMap.get(serviceObj.getKey());
}
主要逻辑:
- 从本地缓存serviceInfoMap获取
- 如果本地缓存中没有,则发送HTTP调用从Nacos服务端获取
- 开启一个定时任务,每隔1秒从Nacos服务端获取最新的服务实例信息, 更新到本地缓存seriveInfoMap中
- 从本地缓存serviceInfoMap中获取服务实例信息
💫从本地缓存获取
private ServiceInfo getServiceInfo0(String serviceName, String clusters) {
String key = ServiceInfo.getKey(serviceName, clusters);
// 本地缓存serviceInfoMap获取
return serviceInfoMap.get(key);
}
就单纯地从本地缓存serviceInfoMap获取
💫发送HTTP调用从Nacos服务端获取
![img](https://img-blog.csdnimg.cn/img_convert/ceedcca2306e59fc3460a23075450c39.png)
![img](https://img-blog.csdnimg.cn/img_convert/9601192930bd60730c5ec89c01e4d74d.png)
![img](https://img-blog.csdnimg.cn/img_convert/2620ac1f0d7b222584bc250ff34ca88e.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**
oMap获取**
#### 💫发送HTTP调用从Nacos服务端获取
[外链图片转存中…(img-DD4Vwy8c-1715284847089)]
[外链图片转存中…(img-b5xID2RF-1715284847090)]
[外链图片转存中…(img-ZKxbmXJ4-1715284847090)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新