【深入理解SpringCloud微服务】深入理解nacos配置中心(五)——客户端监听配置变更并刷新的源码分析

原理回顾

在之前的《宏观理解nacos配置中心原理》这一篇文章中,我们分析过nacos客户端监听配置变更并刷新配置的这一部分的流程。

首先是nacos会给当前客户端引用的每个配置文件都添加一个监听器,这个监听器收到通知会触发配置刷新的操作。

在这里插入图片描述

以上是给被客户端引用的配置文件添加监听器的流程。在SpringBoot启动完成的时候,会发布ApplicationReadyEvent事件,触发Spring的事件监听机制,该事件被NacosContextRefresher接收并处理,然后NacosContextRefresher的onApplicationEvent方法就会给每个被客户端引用的配置文件创建一个CacheData对象,然后在CacheData对象中添加一个上面所说的监听器。

在这里插入图片描述

我们还解析了这个CacheData的结构,包含了配置文件的dataId、group、tenant(也就是namespace)、配置文件内容content,配置文件MD5,监听器列表listeners等成员属性。

既然给每个被客户端引用的配置文件添加了监听器,那么就会有触发该监听器的地方。

在nacos配置中心客户端启动的时候,会创建NacosConfigService,NacosConfigService的构造方法中又会创建ClientWorker,然后ClientWorker初始化的时候会启动定时任务。这个监听器就是在ClientWorker的定时任务中触发的。

在这里插入图片描述
ClientWorker的定时任务流程大概四这样:

  1. 通过http请求获取服务端中发生变更的DataID列表。
  2. 根据这些DataID列表再次请求服务端获取对应的配置文件内容content。
  3. 再把content赋值到CacheData并重新计算MD5值。
  4. 计算出来的MD5值跟CacheData的listeners中每个Listener保存的老MD5值比较,如果不一致,就会调用Listener的receiveConfigInfo()方法。

这里调用Listener的receiveConfigInfo()方法就会调用到NacosContextRefresher添加的监听器的receiveConfigInfo()方法,也就是说监听器就是在这里触发的。

NacosContextRefresher添加的这个监听器的receiveConfigInfo()方法会发布一个RefreshEvent事件。

在这里插入图片描述

发布的RefreshEvent事件会触发Spring的事件监听机制,会被spring-cloud-context包提供的RefreshEventListener监听器接收并处理,触发配置刷新。这里的配置刷新不是nacos实现的,nacos只是发布了一个RefreshEvent事件,后续的都是spring-cloud-context里面的公共逻辑。

因此我们本篇文章只会讲到nacos发布RefreshEvent事件,后续的逻辑在下一篇文章中分析。

源码分析

给当前微服务引用的配置文件创建CacheData并添加监听器

ApplicationReadyEvent事件发布

SpringBoot启动之后,在SpringApplication的run方法执行到最后,会在调用listeners.running(context)时发布ApplicationReadyEvent事件。

SpringApplication#run(java.lang.String…)

	public ConfigurableApplicationContext run(String... args) {
		...

		try {
			listeners.running(context);
		}
		catch (...) {...}
		return context;
	}

SpringApplicationRunListeners#running

	void running(ConfigurableApplicationContext context) {
		for (SpringApplicationRunListener listener : this.listeners) {
			listener.running(context);
		}
	}

EventPublishingRunListener#running

	@Override
	public void running(ConfigurableApplicationContext context) {
		context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context));
		...
	}

在这里插入图片描述

NacosContextRefresher监听ApplicationReadyEvent事件

在spring-cloud-starter-alibaba-nacos-config的spring.factories文件中指定了nacos的自动配置类NacosConfigAutoConfiguration,在NacosConfigAutoConfiguration中通过@Bean注解配置了NacosContextRefresher。

NacosContextRefresher实现了ApplicationListener接口监听ApplicationReadyEvent事件。

在这里插入图片描述

NacosContextRefresher#onApplicationEvent(ApplicationReadyEvent)

NacosContextRefresher#onApplicationEvent(ApplicationReadyEvent)

	public void onApplicationEvent(ApplicationReadyEvent event) {
		if (this.ready.compareAndSet(false, true)) {
			this.registerNacosListenersForApplications();
		}
	}

NacosContextRefresher#registerNacosListenersForApplications()

	private void registerNacosListenersForApplications() {
		if (isRefreshEnabled()) {
			for (NacosPropertySource propertySource : NacosPropertySourceRepository
					.getAll()) {
				...
				// 引用的配置文件对应的dataId
				String dataId = propertySource.getDataId();
				// 注册监听器
				registerNacosListener(propertySource.getGroup(), dataId);
			}
		}
	}

NacosContextRefresher#registerNacosListener(String, String)

	private void registerNacosListener(final String groupKey, final String dataKey) {
		...
		try {
			// 调用NacosConfigService添加监听器
			configService.addListener(dataKey, groupKey, listener);
		}
		catch (...) {...}
	}

沿着NacosContextRefresher的onApplicationEvent方法一路下来,就可以看到调用了NacosConfigService的addListener方法添加监听器。
在这里插入图片描述

NacosConfigService#addListener(String, String, Listener)

    public void addListener(String dataId, String group, Listener listener) throws NacosException {
    	// 调用ClientWorker添加监听器
        worker.addTenantListeners(dataId, group, Arrays.asList(listener));
    }

NacosConfigService的构造方法会创建ClientWorker,NacosConfigService的addListener就是直接调用ClientWorker添加监听器。

在这里插入图片描述

ClientWorker#addTenantListeners(String, String, List<? extends Listener>)

    public void addTenantListeners(String dataId, String group, List<? extends Listener> listeners)
            throws NacosException {
        ...
        // 往ClientWorker的Map中添加一个CacheData并返回
        CacheData cache = addCacheDataIfAbsent(dataId, group, tenant);
        synchronized (cache) {
            for (Listener listener : listeners) {
            	// 往CacheData中添加指定监听器
                cache.addListener(listener);
            }
            ...
        }
    }

ClientWorker这样的的成员变量:

    /**
     * groupKey -> cacheData.
     */
    private final AtomicReference<Map<String, CacheData>> cacheMap = new AtomicReference<Map<String, CacheData>>(
            new HashMap<String, CacheData>());

groupKey就是根据dataId, group, tenant三元组按一定规则拼接而成的字符串,作为Map中该CacheData对应的key,每个dataId, group, tenant三元组对应就是一个配置文件,在Map中都有一个groupKey与CacheData的映射。

ClientWorker的addTenantListeners方法就是创建一个CacheData并放入这个cacheMap中,然后往这个CacheData中添加一个指定的监听器listener。

在这里插入图片描述

CacheData

CacheData的结构我们已经分析过,包含了配置文件的dataId、group、tenant(也就是namespace)、配置文件内容content,配置文件MD5,,监听器列表listeners等成员属性。

public class CacheData {
    
    ...
    
    public final String dataId;
    
    public final String group;
    
    public final String tenant;
    
    private final CopyOnWriteArrayList<ManagerListenerWrap> listeners;
    
    private volatile String md5;
   
   	...
    
    private volatile String content;
        
	...
}

在这里插入图片描述

ClientWorker定时任务

    public NacosConfigService(Properties properties) throws NacosException {
		...
		// 创建ClientWorker
        this.worker = new ClientWorker(this.configFilterChainManager, serverListManager, properties);
		...
        
    }
    public ClientWorker(final ConfigFilterChainManager configFilterChainManager, ServerListManager serverListManager,
            final Properties properties) throws NacosException {
		...
		// 创建ConfigRpcTransportClient
        agent = new ConfigRpcTransportClient(properties, serverListManager);
		...
		// 设置定时任务线程池ScheduledExecutorService
        agent.setExecutor(executorService);
        // 启动定时任务
        agent.start();
        
    }

在NacosConfigService的构造方法会创建ClientWorker,ClientWorker的构造方法会创建一个ConfigRpcTransportClient,给这个ConfigRpcTransportClient设置一个定时任务线程池ScheduledExecutorService,然后调用这个ConfigRpcTransportClient的start()方法启动定时任务。

在这里插入图片描述

ConfigTransportClient#start()

ConfigTransportClient#start()

    public void start() throws NacosException {
        ...
        startInternal();
    }

ClientWorker.ConfigRpcTransportClient#startInternal()

       public void startInternal() {
            executor.schedule(() -> {
                while (!executor.isShutdown() && !executor.isTerminated()) {
                    try {
                    	// 从listenExecutebell这个队列中获取一个元素,阻塞等待5秒,如果没有则返回
                    	// 客户端会在接收到服务端通知时添加一个元素到这个队列中
                        listenExecutebell.poll(5L, TimeUnit.SECONDS);
                        ...
                        // 5秒后还是没有从队列中获取到元素,也往下执行
                        executeConfigListen();
                    } catch (...) {...}
                }
            }, 0L, TimeUnit.MILLISECONDS);
            
        }

ConfigTransportClient的startInternal方法调用上面设置的ScheduledExecutorService执行定时任务,定时任务是一个while循环,每一轮循环先从listenExecutebell这个队列中poll一个元素,这个listenExecutebell队列其实会在客户端收到服务端发送的配置变更通知时,往里面offer一个元素,也就是说如果listenExecutebell.poll()在5秒内返回了,说明收到了服务端发送的配置变更通知,如果5秒后还是没有返回,那么也继续往下执行,调用executeConfigListen()方法。

在这里插入图片描述

ClientWorker.ConfigRpcTransportClient#executeConfigListen()

        public void executeConfigListen() {
            
            ...
                    try {
                        ...
                        // 请求nacos服务端获取发生配置变更的配置文件信息
                        ConfigChangeBatchListenResponse configChangeBatchListenResponse = (ConfigChangeBatchListenResponse) requestProxy(rpcClient, configChangeListenRequest);
                        if (configChangeBatchListenResponse.isSuccess()) {
                            
                            ...
                            if (!CollectionUtils.isEmpty(configChangeBatchListenResponse.getChangedConfigs())) {
                                ...
                                // 遍历每个发生变更的配置
                                for (ConfigChangeBatchListenResponse.ConfigContext changeConfig : configChangeBatchListenResponse
                                        .getChangedConfigs()) {
                                    ...
                                    // 刷新配置
                                    refreshContentAndCheck(changeKey, !isInitializing);
                                }
                                
                            }
                            ...
        }

ConfigTransportClient的executeConfigListen方法会请求服务端获取发生配置变更的配置文件信息,然后遍历每个发生变更的配置,调用refreshContentAndCheck方法刷新配置。

在这里插入图片描述

然后进入到ClientWorker的refreshContentAndCheck方法。

ClientWorker#refreshContentAndCheck(java.lang.String, boolean)

    private void refreshContentAndCheck(String groupKey, boolean notify) {
    	// 根据groupKey拿到CacheData
    	// 这个groupKey也是根据dataId, group, tenant三元组拼接而成的,与放入cacheMap时的拼接规则一样
        CacheData cache = cacheMap.get().get(groupKey);
        if (cache != null) {
            refreshContentAndCheck(cache, notify);
        }
    }

ClientWorker的refreshContentAndCheck方法从ClientWorker的cacheMap中根据groupKey获取对应的CacheData。这个groupKey也是根据dataId, group, tenant三元组拼接而成的,与放入cacheMap时的拼接规则是一样的。然后调用refreshContentAndCheck(cache, notify),这个cache参数就是CacheData。

在这里插入图片描述

ClientWorker#refreshContentAndCheck(CacheData, boolean)

   private void refreshContentAndCheck(CacheData cacheData, boolean notify) {
        try {
        	// 发起RPC远程调用,获取变更后的配置文件内容
            ConfigResponse response = getServerConfig(cacheData.dataId, cacheData.group, cacheData.tenant, 3000L,
                    notify);
            ...
            // 变更后的配置文件内容更新到cacheData,同时会更新cacheData中的md5值
            cacheData.setContent(response.getContent());
           ...
           // 触发cacheData中的监听器
            cacheData.checkListenerMd5();
        } catch (...) {...}
    }

ClientWorker的refreshContentAndCheck方法首先发起RPC远程调用,获取变更后的配置文件内容,然后把变更后的配置文件内容更新到cacheData,最后调用cacheData.checkListenerMd5()触发cacheData中的监听器。

在这里插入图片描述

CacheData#checkListenerMd5()

    void checkListenerMd5() {
    	// 遍历CacheData中的每一个监听器
        for (ManagerListenerWrap wrap : listeners) {
        	// 比对CacheData中新的md5值和监听器中保存的旧md5值
            if (!md5.equals(wrap.lastCallMd5)) {
            	// 比对不相等,执行safeNotifyListener方法
                safeNotifyListener(dataId, group, content, type, md5, encryptedDataKey, wrap);
            }
        }
    }

checkListenerMd5方法会遍历CacheData中的每一个监听器,比对CacheData中新的md5值和监听器中保存的旧md5值,如果不相等,执行safeNotifyListener方法。

在这里插入图片描述

CacheData#safeNotifyListener(…)

    private void safeNotifyListener(..., final ManagerListenerWrap listenerWrap) {
        final Listener listener = listenerWrap.listener;
        ...
        		// 调用监听器的receiveConfigInfo方法
                listener.receiveConfigInfo(contentTmp);
                ...
                // 更新监听器的md5值
                listenerWrap.lastCallMd5 = md5;
                ...
        };

AbstractSharedListener#receiveConfigInfo(String)

listener.receiveConfigInfo(contentTmp)里面会调用到innerReceive方法,就进入了在NacosContextRefresher的registerNacosListener方法创建的监听器实现的innerReceive方法。

在这里插入图片描述

com.alibaba.cloud.nacos.refresh.NacosContextRefresher#registerNacosListener(String, String)

	private void registerNacosListener(final String groupKey, final String dataKey) {
		...
		Listener listener = listenerMap.computeIfAbsent(key,
				lst -> new AbstractSharedListener() {
					@Override
					public void innerReceive(String dataId, String group,
							String configInfo) {
						...
						// 发布一个RefreshEvent事件
						applicationContext.publishEvent(
								new RefreshEvent(this, null, "Refresh Nacos config"));
						...
					}
				});
		...
	}

这里的监听器的innerReceive方法被触发,发布一个RefreshEvent事件。

在这里插入图片描述

发布的RefreshEvent事件会触发Spring的事件监听机制,被spring-cloud-context包提供的RefreshEventListener监听器接收并处理,后续就是spring-cloud-context里面关于配置刷新的公共逻辑,在下一篇文章中分析。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值