Nacos 服务发现源码:藏在背后的两套事件机制,多数文章只讲了一半
一句话结论 :Nacos 服务发现里同时跑着两套完全独立、互不知道对方存在的事件系统------一套是 Spring 容器的
ApplicationEvent,另一套是 Nacos 客户端 SDK 自带的NotifyCenter。网上九成文章只讲到「AbstractAutoServiceRegistration监听WebServerInitializedEvent触发注册」就结束了,而这只是其中一套、也是不太关键的那一半。读完你能获得:
① 一张分清两套事件系统边界的全局结构图;
② 从注册到变更推送的完整源码调用链;
③ 解释「服务下线了消费端为什么还慢半拍」的根因,以及优雅下线、注册时序这两个生产坑的应对方法。
适用版本:Spring Boot 3.x / Spring Cloud 2022.x / Spring Cloud Alibaba 2022.x / Nacos Client 2.x。涉及版本差异处单独标注。
0. 整体视图:先建立全局认知,两套事件系统
很多人对 Nacos 事件机制的认知是模糊的,根源在于把「Spring 的事件」和「Nacos 的事件」当成了一回事。先把这两套系统理清楚,后面的源码才不会越看越乱:
arduino
┌──────────────────────── Provider(服务提供者) ────────────────────────┐
│ │
│ Spring 容器 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ApplicationEvent 体系(org.springframework.context.*) │ │
│ │ 作用:决定「何时」把自己注册进 Nacos │ │
│ │ 关键事件:WebServerInitializedEvent │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ register() │
└──────────────────────────┼────────────────────────────────────────────┘
▼
┌─────────────┐
│ Nacos Server │ ← 注册表 + 变更推送源
└─────────────┘
│ push(gRPC,Nacos 2.x)
┌──────────────────────────┼────────────────────────────────────────────┐
│ ▼ │
│ Nacos Client SDK │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ NotifyCenter 体系(com.alibaba.nacos.common.notify.*) │ │
│ │ 作用:实例变更后「推送」给本地订阅者、刷新本地缓存 │ │
│ │ 关键事件:InstancesChangeEvent │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────── Consumer(服务消费者) ───────────────────────┘
一句话先记住:
- Spring 的
ApplicationEvent解决的是「时机」问题------服务在容器生命周期的哪个点,才适合把自己注册/注销。 - Nacos 的
NotifyCenter解决的是「推送」问题------注册表里的实例变了,怎么异步通知到本地的订阅者并刷新缓存。
两套系统的包名完全不同(org.springframework.context vs com.alibaba.nacos.common.notify),底层实现也完全不同(一个是同步多播器为主,一个是守护线程 + 阻塞队列的异步模型)。它们唯一的交点在消费端的负载均衡缓存上,我们最后再讲。
1. Provider 侧:Spring 事件决定「何时注册」
1.1 注册本身不需要事件,需要事件的是「时机」
把一个实例注册进 Nacos,本质就一行:
java
namingService.registerInstance(serviceId, group, instance);
那为什么要扯上事件机制?因为注册的时机很讲究。如果在 Spring 容器还在初始化、内嵌 Web 容器(Tomcat/Undertow)端口都没绑定好的时候就注册,等于把一个还没法对外提供服务的实例暴露给了消费者------消费者拿到 IP:Port 一调,连接被拒。
所以注册动作必须挂在「Web 容器已经起来、端口已经绑定 」这个时间点上。Spring Cloud 用 WebServerInitializedEvent 来精确表达这个时刻。
1.2 AbstractAutoServiceRegistration 的源码骨架
核心抽象类在 spring-cloud-commons 里,注意它的类声明------它本身就是一个 ApplicationListener:
java
public abstract class AbstractAutoServiceRegistration<R extends Registration>
implements AutoServiceRegistration,
ApplicationContextAware,
ApplicationListener<WebServerInitializedEvent> { // ← 监听这个事件
@Override
@SuppressWarnings("deprecation")
public void onApplicationEvent(WebServerInitializedEvent event) {
bind(event);
}
public void bind(WebServerInitializedEvent event) {
ApplicationContext context = event.getApplicationContext();
// management 端口(actuator 独立端口)不参与业务注册,直接 return
if (context instanceof ConfigurableWebServerApplicationContext) {
if ("management".equals(((ConfigurableWebServerApplicationContext) context)
.getServerNamespace())) {
return;
}
}
// 绑定真实端口,CAS 保证只设置一次
this.port.compareAndSet(0, event.getWebServer().getPort());
this.start();
}
}
这里有两个细节值得停一下:
managementnamespace 的过滤 。如果你给 actuator 配了独立端口(management.server.port),它会触发一个独立的WebServerInitializedEvent。这段判断保证业务端口和管理端口不会重复触发注册。port.compareAndSet(0, port)。用 CAS 而不是直接赋值,是为了应对可能的重复事件,保证端口只被「第一次」初始化。这是框架级代码里很典型的防御性写法。
1.3 完整调用链:从容器启动到注册成功
把链路串起来(Nacos 实现类用 NacosServiceRegistry):
scss
Spring Boot 启动
│
▼
WebServerStartStopLifecycle.start()
│ this.webServer.start(); // 内嵌容器启动、端口绑定
│ applicationContext.publishEvent(new ServletWebServerInitializedEvent(...))
▼
AbstractAutoServiceRegistration.onApplicationEvent(WebServerInitializedEvent)
│
▼ bind() → start()
│
▼
NacosServiceRegistry.register(Registration)
│ Instance instance = getNacosInstanceFromRegistration(registration);
│ namingService.registerInstance(serviceId, group, instance);
▼
注册成功,实例进入 Nacos 注册表
到这里,Provider 侧用到的是纯粹的 Spring 事件机制 ,跟 Nacos 自己那套 NotifyCenter 一点关系都没有。这点务必分清楚。
1.4 追问:为什么是 WebServerInitializedEvent,不是 ContextRefreshedEvent?
面试或者 Code Review 时这是个能区分深度的问题。两个事件的触发时机不同:
ContextRefreshedEvent:Spring 容器刷新完成(所有单例 Bean 初始化完毕),但此时内嵌 Web 容器还没必然 start,端口可能还没绑定。WebServerInitializedEvent:Web 容器已经 start、端口已经绑定。
注册必须等端口就绪,所以选 WebServerInitializedEvent 是对的。这个时序差异不是抠字眼------它在生产里真的会出事,第四节细说。
2. Consumer 侧:Nacos 自己的 NotifyCenter 在推送变更
消费端订阅某个服务、感知它的实例上下线,主角彻底换成了 Nacos SDK ,Spring 的 ApplicationListener 在这条链路上基本退场。
2.1 入口:NacosNamingService.init() 里注册了发布者与订阅者
整套消费端事件机制的「装配入口」,在 NacosNamingService 的 init() 方法里。Nacos 2.x 的关键几行:
java
private void init(Properties properties) throws NacosException {
// ... 省略参数校验、namespace 解析等 ...
// ① 每个 NacosNamingService 实例生成一个唯一事件作用域(多实例隔离用,2.6 节展开)
this.notifierEventScope = UUID.randomUUID().toString();
// ② 创建实例变更通知器(它是一个 Subscriber)
this.changeNotifier = new InstancesChangeNotifier(this.notifierEventScope);
// ③ 向 NotifyCenter 注册 InstancesChangeEvent 的发布者,队列容量 16384
NotifyCenter.registerToPublisher(InstancesChangeEvent.class, 16384);
// ④ 把 changeNotifier 注册为该事件的订阅者
NotifyCenter.registerSubscriber(this.changeNotifier);
// ⑤ 本地服务信息缓存的持有者
this.serviceInfoHolder = new ServiceInfoHolder(namespace, this.notifierEventScope, ...);
// ⑥ 客户端代理,内部根据情况走 gRPC(NamingGrpcClientProxy)或 HTTP
this.clientProxy = new NamingClientProxyDelegate(
this.namespace, serviceInfoHolder, properties, changeNotifier);
}
版本提示:Nacos 1.x 里持有 notifier 的不是
ServiceInfoHolder而是HostReactor(com.alibaba.nacos.client.naming.core.HostReactor)。如果你翻到的源码里出现HostReactor,说明在看 1.x;2.x 把缓存与推送处理重构进了ServiceInfoHolder。这是版本差异里最容易让人对不上号的一处。
到这一步,NotifyCenter 内部就建立起了「InstancesChangeEvent → DefaultPublisher → InstancesChangeNotifier」这条发布订阅关系。
2.2 NotifyCenter 的底层:守护线程 + 阻塞队列的异步模型
这是 Nacos 事件机制和 Spring 事件机制最大的实现差异。Spring 的 SimpleApplicationEventMulticaster 默认是同步 多播(不配 taskExecutor 时,发布线程直接挨个调用监听器)。而 Nacos 的 NotifyCenter 默认是异步的:
NotifyCenter是门面类,内部维护Map<String, EventPublisher> publisherMap,key 是事件的全限定类名(com.alibaba.nacos.client.naming.event.InstancesChangeEvent),value 是DefaultPublisher。DefaultPublisher本身继承自Thread,内部维护一个BlockingQueue(默认容量 16384)。它的run()是个死循环,不断从队列里取事件、回调订阅者。- 发布事件时(
publishEvent),事件被offer进队列;如果队列满了入队失败,则降级为当前线程直接receiveEvent同步处理,保证不丢事件。
这意味着:你的 EventListener 回调默认运行在 Nacos 的事件线程上,不是你的业务线程,也不是 Spring 的线程 。在回调里别做重活、别抛异常不处理、别阻塞------否则会拖慢整个事件队列的消费。如果回调耗时,应该自己提交线程池异步处理(Nacos 也允许你给 EventListener 指定 getExecutor())。
2.3 触发点:实例变更如何变成一个 InstancesChangeEvent
事件从哪儿冒出来的?答案是 ServiceInfoHolder.processServiceInfo()------无论是 gRPC 推送来的新实例列表,还是轮询拉回来的,最终都会进这个方法做「本地缓存处理」:
java
public ServiceInfo processServiceInfo(ServiceInfo serviceInfo) {
String serviceKey = serviceInfo.getKey();
if (serviceKey == null) {
return null;
}
ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
// 空推送保护:避免因为一次异常的空列表把本地缓存清空(pushEmptyProtection)
if (isEmptyOrErrorPush(serviceInfo)) {
return oldService;
}
// 写入本地缓存
serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
// 关键:对比新旧实例,判断是否真的变了
boolean changed = isChangedServiceInfo(oldService, serviceInfo);
if (changed) {
// 只有真的发生变化,才发布事件
NotifyCenter.publishEvent(new InstancesChangeEvent(
notifierEventScope,
serviceInfo.getName(),
serviceInfo.getGroupName(),
serviceInfo.getClusters(),
serviceInfo.getHosts()));
}
// 落本地容灾文件
return serviceInfo;
}
注意 isEmptyOrErrorPush 这个空推送保护 :生产中如果 Server 因为某种异常推了个空列表过来,没有保护的话本地缓存会被清空,导致消费端瞬间「找不到任何提供者」。这是 Nacos 后来补上的防御逻辑,也是设计注册中心客户端缓存时值得借鉴的一个点------缓存的更新要对「异常的空值」保持警惕。
2.4 两种触发路径:gRPC 推送(2.x 主力)+ 定时轮询(兜底)
processServiceInfo 会被两条路径调用,这也是 Nacos 1.x → 2.x 的核心演进:
路径一:gRPC 主动推送(Nacos 2.x 的默认)
arduino
Server 实例变更
→ gRPC 长连接推送
→ 客户端 NamingPushRequestHandler 接收
→ serviceInfoHolder.processServiceInfo(serviceInfo)
→ 发布 InstancesChangeEvent
路径二:定时轮询(UpdateTask,作为兜底)
ServiceInfoUpdateService 里的 UpdateTask 会被调度执行,主动去 Server 拉最新实例列表,同样走 processServiceInfo。看一下它的调度常量(Nacos Client 2.2.3):
- 正常间隔 =
serviceObj.getCacheMillis() × 6(DEFAULT_UPDATE_CACHE_TIME_MULTIPLE = 6)。cacheMillis由 Server 下发,客户端ServiceInfo初始默认1000L但会被服务端值覆盖,所以实际间隔是配置相关的,不是一个固定秒数。 - 出错或拉到空列表时走指数退避:
executor.schedule(this, Math.min(delayTime << failCount, DEFAULT_DELAY * 60), ...),DEFAULT_DELAY = 1000,退避上限 60 秒。
特别提醒:这个 6 是「倍数 」不是「秒数 」。网上不少文章写成「UpdateTask 每 6 秒轮询一次」,是把 DEFAULT_UPDATE_CACHE_TIME_MULTIPLE 误读了------真正的间隔取决于服务端下发的 cacheMillis 再乘以 6。
这就是为什么 Nacos 2.x 的实时性比 1.x 好很多:1.x 靠 UDP 推送 + 轮询,2.x 升级成 gRPC 长连接主动推送,轮询退居兜底。但兜底依然存在------理解这点,排查「为什么实例下线了消费端还是隔了一会儿才感知」时才有方向:先看 gRPC 长连接是否健康,再看轮询周期。
2.5 InstancesChangeNotifier 回调到你的 EventListener
DefaultPublisher 取出 InstancesChangeEvent,回调订阅者 InstancesChangeNotifier.onEvent(),它再根据「服务标识」找到你注册的所有 EventListener 并逐个回调:
java
public class InstancesChangeNotifier extends Subscriber<InstancesChangeEvent> {
// key = groupName@@serviceName@@clusters,value = 监听该服务的 EventListener 集合
private final Map<String, ConcurrentHashSet<EventListener>> listenerMap
= new ConcurrentHashMap<>();
public void registerListener(String groupName, String serviceName,
String clusters, EventListener listener) {
String key = ServiceInfo.getKey(
NamingUtils.getGroupedName(serviceName, groupName), clusters);
listenerMap.computeIfAbsent(key, k -> new ConcurrentHashSet<>()).add(listener);
}
@Override
public void onEvent(InstancesChangeEvent event) {
String key = ServiceInfo.getKey(
NamingUtils.getGroupedName(event.getServiceName(), event.getGroupName()),
event.getClusters());
ConcurrentHashSet<EventListener> listeners = listenerMap.get(key);
if (listeners != null) {
for (EventListener listener : listeners) {
// 回调到你写的 onEvent
final NamingEvent namingEvent = /* 构建 NamingEvent */ ...;
if (listener.getExecutor() != null) {
listener.getExecutor().execute(() -> listener.onEvent(namingEvent));
} else {
listener.onEvent(namingEvent);
}
}
}
}
}
而你作为业务方介入的方式,是 Nacos 的公开 API ------再强调一次,这里的 EventListener / NamingEvent 全部来自 com.alibaba.nacos.api.naming.listener.*,和 Spring 没有任何关系:
java
NamingService naming = NacosFactory.createNamingService(serverAddr);
naming.subscribe("court-booking-service", new EventListener() {
@Override
public void onEvent(Event event) {
if (event instanceof NamingEvent namingEvent) {
List<Instance> instances = namingEvent.getInstances();
// 实例上下线时你的自定义逻辑:刷新自维护路由、上下线告警、灰度切换等
}
}
// 可选:指定线程池,避免阻塞 Nacos 事件线程
@Override
public Executor getExecutor() {
return myBizExecutor;
}
});
2.6 eventScope 如何做多实例隔离
2.1 里那个 UUID.randomUUID().toString() 不是随手写的。设想你在一个进程里创建了两个 NacosNamingService(比如连了两套注册中心)------它们都订阅 InstancesChangeEvent,那 NotifyCenter 怎么知道某个事件该投递给哪个实例的订阅者?
答案是 Event Scope(事件作用域) 。每个 NacosNamingService 在 init 时生成一个随机 notifierEventScope,它同时被用来构建该实例的 InstancesChangeNotifier 和 ServiceInfoHolder。发布事件时事件带上这个 scope,DefaultPublisher 投递前先做 scopeMatches 校验:
java
@Override
public boolean scopeMatches(InstancesChangeEvent event) {
return this.eventScope.equals(event.scope());
}
只有 scope 匹配的订阅者才会收到------这样多个 NamingService 实例的事件就互不串台。这是个隔离做得很干净的设计,面试聊到「Nacos 事件机制如何支持多实例」时,能讲到这层就很加分了。
3. 两套系统的边界与桥接:LoadBalancer 缓存为什么「慢半拍」
现在把两套系统接起来。这里有个问题:既然 Nacos 客户端能通过 InstancesChangeEvent 实时感知实例变更,为什么实践中服务下线后,消费端有时还是会调到已经死掉的实例?
根因在于 Spring Cloud LoadBalancer 有自己的一层缓存。CachingServiceInstanceListSupplier 会缓存服务实例列表,默认 TTL 35 秒 (spring.cloud.loadbalancer.cache.ttl;spring-cloud-commons 4.0.x 的 LoadBalancerCacheProperties 里就是 Duration.ofSeconds(35))。也就是说:Nacos 本地缓存已经更新了,但 LoadBalancer 这层缓存还没过期,于是负载均衡器仍然可能选中那个已下线的实例。这就是「两套事件系统的边界」带来的真实代价------Nacos 的 NotifyCenter 不会自动去刷新 Spring 这边的 LoadBalancer 缓存。
历史上 Spring Cloud Alibaba 通过一个适配组件(早期版本是 NacosWatch,定时把变更转译成 Spring 的 HeartbeatEvent 发布,相关缓存监听后刷新)来做桥接。但这块跨 SCA 版本变动较大 :Nacos 2.x 转 gRPC 推送后,watch / 心跳的具体实现和默认行为在不同 SCA 版本里并不一致。所以这里我不写死结论------请务必在你自己依赖的 spring-cloud-starter-alibaba-nacos-discovery 版本里,搜一下 NacosWatch、HeartbeatEvent、RefreshRoutesEvent 的引用,确认你这个版本的桥接路径。这也是写源码文章时该有的态度:宁可让读者自己核对版本,也不要给一个可能过时的断言。
如果你确实需要「实例一变更,消费端立刻刷新」,一个被验证过的实战思路是:模仿 InstancesChangeNotifier,自己订阅 InstancesChangeEvent,在回调里主动让 LoadBalancer 缓存失效。注意这是「绕过默认 TTL 的优化手段」,上生产前要评估额外的刷新频率开销,别为了几秒的实时性把缓存机制本身的收益抵消掉。
顺带一提,Spring Cloud Gateway 也有同类问题:CachingRouteLocator 监听的是 Spring 的 RefreshRoutesEvent,服务上下线时若路由缓存没及时刷新,会出现短暂 404。解决思路同源------找到那个负责把「下游变更」翻译成「RefreshRoutesEvent」的环节。
4. 生产踩坑:注册时序与优雅下线
4.1 注册成功 ≠ 业务就绪
回到 Provider 侧那个时序问题。社区有个经典案例(spring-cloud-alibaba issue #1805):Dubbo + Nacos 混用时,Nacos 自动注册监听的是 WebServerInitializedEvent,而 Dubbo 服务暴露监听的是 ContextRefreshedEvent,前者触发早于后者。结果就是:Nacos 已经把实例注册上去、Server 已经向订阅者推送了变更,但此时该实例自身的 Dubbo 服务可能还没暴露完成 ,消费端拿到实例却调不通,报 No provider available。
抽象出来的教训对任何微服务都成立:「注册到注册中心」和「业务真正就绪」是两件事,中间存在一个危险窗口。 应对做法有几种:
- 把重初始化逻辑(缓存预热、连接池建立、库存预加载等)放在注册动作之前完成;
- 或者用 Nacos 实例的健康状态 / 权重,让实例在「未就绪」时不被分发流量,就绪后再开放;
- 配合 Spring Boot 的 readiness probe,让编排层(K8s)也理解就绪状态。
4.2 优雅下线:又一次用到 Spring 事件
发版时直接 kill -9 进程,实例不能立刻从注册表消失,这段窗口里消费端会持续打到一个已经不存在的实例。
版本差异:Nacos 1.x 临时实例靠心跳维持,断连后要等心跳超时(秒级到十几秒不等)才被剔除;Nacos 2.x 临时实例改用 gRPC 长连接,连接断开后 Server 的检测通常更快,但仍非「零延迟」,且
kill -9是否优雅退出取决于连接关闭方式。无论哪个版本,不主动注销就会留下这段空窗。
正确做法是主动注销,而注销的时机,又回到了 Spring 的事件机制:
java
@Component
public class GracefulDeregister {
@Autowired
private NacosServiceRegistry registry;
@Autowired
private NacosRegistration registration;
@PreDestroy // 或监听 ContextClosedEvent
public void deregister() {
registry.deregister(registration); // 主动从 Nacos 摘除
// 再配合 server.shutdown=graceful,等存量请求处理完
}
}
@PreDestroy / ContextClosedEvent 触发主动注销 → 实例立刻从注册表消失 → 配合优雅停机等存量请求处理完。这是 Spring 事件机制在 Nacos 场景下「另一半」的应用,和注册时的 WebServerInitializedEvent 首尾呼应。
5. 设计思想:四点可迁移的设计判断
源码看完,比记住类名更值钱的是这几条可迁移的设计判断------它们在你自己设计中间件客户端、缓存、事件系统时同样成立:
- 同步多播 vs 异步队列,是一次明确的取舍 。Spring 默认同步多播,胜在简单、调用栈清晰、异常好定位;Nacos 选守护线程 + 阻塞队列的异步模型,胜在发布方不被订阅者拖慢、天然削峰。代价是回调脱离了发布线程上下文(所以才有「别在回调里做重活」的约束)。没有更好,只有更适合:注册中心的变更推送是高频、订阅方处理时长不可控的场景,异步是合理选择。
- 客户端缓存要对「异常的空值」保持警惕 。
isEmptyOrErrorPush的空推送保护,本质是「不让一次异常输入摧毁已有的好状态」。任何「拉取远端数据刷新本地缓存」的设计都该问一句:远端返回空/异常时,我是覆盖还是保留? - 用 scope 做事件隔离,比用多个事件总线更轻。Nacos 没有为每个 NamingService 建一套独立 NotifyCenter,而是给事件打 scope 标签、投递前匹配。这是「共享基础设施 + 逻辑隔离」的典型手法,比物理隔离省资源。
- 解耦的代价是边界延迟。两套事件系统互不感知带来了清晰的职责边界,但也制造了 LoadBalancer 缓存那段「慢半拍」。架构上每一次解耦,都要问一句:被切开的两端,靠什么、隔多久才能重新对齐?
6. 总结
把这篇的核心收束成三句话:
- Provider 侧 靠 Spring 的
ApplicationEvent(WebServerInitializedEvent)解决「注册时机」,注册/注销都挂在容器生命周期事件上; - Consumer 侧 靠 Nacos 自带的
NotifyCenter(InstancesChangeEvent)解决「变更推送」,这是一套守护线程 + 阻塞队列的异步系统,和 Spring 无关; - 两套系统的边界 落在消费端的负载均衡缓存上,默认存在数十秒级的感知延迟,桥接机制随 SCA 版本变化------需要实时性时,可以自己订阅
InstancesChangeEvent主动刷新,但要评估代价。
下次再看到「Nacos 事件机制」这个词,先问自己一句:你说的是 Spring 那套,还是 Nacos 那套?能把这个边界讲清楚,你对这个框架的理解就已经超过大部分人了。
引用与延伸阅读
源码版本锚点:Spring Boot 3.x / Spring Cloud 2022.x / Spring Cloud Alibaba 2022.x / Nacos Client 2.x。文中所有断言以此版本为准;标注「以源码为准」处请在你的实际依赖版本中核对。
关键类与模块(便于读者自行下钻):
| 类 | 所属模块 | 职责 |
|---|---|---|
AbstractAutoServiceRegistration |
spring-cloud-commons | 监听 WebServerInitializedEvent,驱动注册 |
NacosServiceRegistry / NacosRegistration |
spring-cloud-alibaba | Nacos 注册/注销实现 |
NacosNamingService |
nacos-client | 消费端装配入口(init()) |
ServiceInfoHolder |
nacos-client | 本地缓存 + processServiceInfo 触发事件 |
NotifyCenter / DefaultPublisher |
nacos-common | 异步事件总线(守护线程 + 阻塞队列) |
InstancesChangeNotifier |
nacos-client | InstancesChangeEvent 订阅者,回调业务 EventListener |
ServiceInfoUpdateService |
nacos-client | 轮询兜底(UpdateTask) |
CachingServiceInstanceListSupplier |
spring-cloud-loadbalancer | 消费端实例列表缓存(边界延迟来源) |
外部参考:
- spring-cloud-alibaba issue #1805(Dubbo + Nacos 注册时序问题)
- Nacos 官方文档 · 客户端通信模型(1.x → 2.x gRPC 演进)
- Spring Cloud Alibaba / Spring Cloud LoadBalancer 官方文档