Nacos源码之配置中心实现

418 阅读10分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

前面已经了解过Nacos中注册中心的实现,今天我们就来了解一下在Naocs中的另一个核心功能配置中心。为什么需要配置中心?

一般在单体架构的时候我们可以将配置写在配置文件中,但缺点就是每次修改配置都需要重启服务才能生效。 在微服务的架构下,可能存在上百甚至更多的服务,如果每次修改一个配置都需要重启所有的服务,那么就会增加系统的不稳定性和维护的成本。

Nacos中如何使用配置中心

1-启动服务端

这里我们在本地启动,并且基于mysql来持久化配置。先修改config模块的配置,并且执行其初始化的sql,nacos-db.sql

### Count of DB:
db.num=1

### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user=root
db.password=bYR-KFa-AEJ-Y9U2018

2-通过客户端来发布和获取配置信息

客户端代码如下:

public static void main(String[] args) throws NacosException, InterruptedException {
	String serverAddr = "localhost";
	String dataId = "test";
	String group = "DEFAULT_GROUP";
	Properties properties = new Properties();
	properties.put("serverAddr", serverAddr);
	ConfigService configService = NacosFactory.createConfigService(properties);
	String content = configService.getConfig(dataId, group, 5000);
	System.out.println(content);
	//客户端订阅
	configService.addListener(dataId, group, new Listener() {
		@Override
		public void receiveConfigInfo(String configInfo) {
			System.out.println("receive:" + configInfo);
		}
		@Override
		public Executor getExecutor() {
			return null;
		}
	});

        //发布配置
	boolean isPublishOk = configService.publishConfig(dataId, group, "content");
	System.out.println(isPublishOk);

	Thread.sleep(3000);
        //获取配置信息 
	content = configService.getConfig(dataId, group, 5000);
	System.out.println(content);
}

大家也可以直接去nacos的web页面去编辑和查看。

Nacos中配置模型

先来了解一下在Nacos配置中心中主要有哪些基本概念:

命名空间(Namespace):

不同的命名空间下,可以存在相同的 Group 或 Data ID 的配置。如针对不同的环境如dev,test,prod。

**配置组(**Group)

代表配置的一种维度。配置分组的常见场景:不同的应用或组件使用了相同的配置项。

配置 ID(Data ID)

代表了具体的配置文件信息。

Nacos配置中心一致性的分析

配置中心一般来说都是非强一致性的,因此会采用 AP ⼀致性协议。

对于配置中心来说,我们主要关注一下两个部分

1-多个服务之间数据是如何保持一致的

2-客户端是如何和服务端之间保持一致的,也可以理解为配置在服务端修改之后是如何通知到各个客户端的。

多个服务之间数据一致性

Nacos配置中心在AP协议下,多个Server都是对等的。数据写入任何一个Server,都会先进行持久化,持久化成功后异步通知其他节点到数据库中拉取最新配置值,并且通知写入成功。

具体持久化的表名称是config_info:

INSERT INTO `nacos`.`config_info`(`id`, `data_id`, `group_id`, `content`, `md5`, `gmt_create`, `gmt_modified`, `src_user`, `src_ip`, `app_name`, `tenant_id`, `c_desc`, `c_use`, `effect`, `type`, `c_schema`, `encrypted_data_key`) VALUES (2, 'test', 'DEFAULT_GROUP', 'content', '9a0364b9e99bb480dd25e1f0284c8555', '2023-01-18 04:12:22', '2023-01-18 04:12:22', NULL, '192.168.5.196', '', '', NULL, NULL, NULL, 'text', NULL, '');

具体的代码可以看下,首先从上面的demo中找到发布配置的相关代码:

//入口
configService.publishConfig(dataId, group, "content");


@Override
public boolean publishConfig(String dataId, String group, String tenant, String appName, String tag,
		String betaIps, String content, String encryptedDataKey, String casMd5, String type)
		throws NacosException {
	try {
                //构造发布的请求request
		ConfigPublishRequest request = new ConfigPublishRequest(dataId, group, tenant, content);
		request.setCasMd5(casMd5);
		request.putAdditionalParam(TAG_PARAM, tag);
		request.putAdditionalParam(APP_NAME_PARAM, appName);
		request.putAdditionalParam(BETAIPS_PARAM, betaIps);
		request.putAdditionalParam(TYPE_PARAM, type);
		request.putAdditionalParam(ENCRYPTED_DATA_KEY_PARAM, encryptedDataKey == null ? "" : encryptedDataKey);
		ConfigPublishResponse response = (ConfigPublishResponse) requestProxy(getOneRunningClient(), request);
		if (!response.isSuccess()) {
			LOGGER.warn("[{}] [publish-single] fail, dataId={}, group={}, tenant={}, code={}, msg={}",
					this.getName(), dataId, group, tenant, response.getErrorCode(), response.getMessage());
			return false;
		} else {
			LOGGER.info("[{}] [publish-single] ok, dataId={}, group={}, tenant={}, config={}", getName(),
					dataId, group, tenant, ContentUtils.truncateContent(content));
			return true;
		}
	} catch (Exception e) {
		LOGGER.warn("[{}] [publish-single] error, dataId={}, group={}, tenant={}, code={}, msg={}",
				this.getName(), dataId, group, tenant, "unknown", e.getMessage());
		return false;
	}
}s

在通过客户端代码发布配置的时候,最终就是需要想Nacos服务端发送请求,在2.x的版本中Nacos中客户端和服务端的通信是依赖GRPC来实现的,最终也会调用到GrpcConnection类去发送请求到服务端。

根据前面的介绍,服务端会处理当前客户端发送的ConfigPublishRequest,并且先进行持久化。

一般来说rpc的服务端对请求的类型都有对应的handler进行处理,这里在config模块中直接找到ConfigPublishRequestHandler类:

handle(ConfigPublishRequest request, RequestMeta meta)

具体的处理都在其handle方法中,这里展示部门代码:

if (StringUtils.isNotBlank(request.getCasMd5())) {
	boolean casSuccess = persistService
			.insertOrUpdateCas(srcIp, srcUser, configInfo, time, configAdvanceInfo, false);
	if (!casSuccess) {
		return ConfigPublishResponse.buildFailResponse(ResponseCode.FAIL.getCode(),
				"Cas publish fail,server md5 may have changed.");
	}
} else {
	persistService.insertOrUpdate(srcIp, srcUser, configInfo, time, configAdvanceInfo, false);
}
ConfigChangePublisher.notifyConfigChange(
		new ConfigDataChangeEvent(false, dataId, group, tenant, time.getTime()));

可以看到执行了insertOrUpdate来新增或者修改客户端发布的配置到db中。并且调用了notifyConfigChange方法来发布了一个ConfigDataChangeEvent事件。

有了事件发布,那么必定就会有消费。在Nacos中大量使用了这种编程模式,可以参考之前相关的介绍

这里其实调用了DefaultPublisher类中的publish方法。

@Override
public boolean publish(Event event) {
	checkIsStart();
	boolean success = this.queue.offer(event);
	if (!success) {
		LOGGER.warn("Unable to plug in due to interruption, synchronize sending time, event : {}", event);
		receiveEvent(event);
		return true;
	}
	return true;
}

DefaultPublisher自身就是一个线程,其run方法中回去不断的从queue中获取event进行消费。属于典型的生产消费模型:

@Override
public void run() {
	openEventHandler();
}

//具体实现
for (; ; ) {
	if (shutdown) {
		break;
	}
	final Event event = queue.take();
	receiveEvent(event);
	UPDATER.compareAndSet(this, lastEventSequence, Math.max(lastEventSequence, event.sequence()));
}

在持久化数据后,对于服务之间的数据同步采用了异步的方式,提高整体的性能。

最终会调用对应的事件订阅方法进行处理当前事件

@Override
public void notifySubscriber(final Subscriber subscriber, final Event event) {
	
	LOGGER.debug("[NotifyCenter] the {} will received by {}", event, subscriber);
	
	final Runnable job = () -> subscriber.onEvent(event);
	final Executor executor = subscriber.executor();
	
	if (executor != null) {
		executor.execute(job);
	} else {
		try {
			job.run();
		} catch (Throwable e) {
			LOGGER.error("Event callback exception: ", e);
		}
	}
}

这里在AsyncNotifyService类中找到对应事件的回调方法onEvent方法:

// Register A Subscriber to subscribe ConfigDataChangeEvent.
NotifyCenter.registerSubscriber(new Subscriber() {
	
	@Override
	public void onEvent(Event event) {
		// Generate ConfigDataChangeEvent concurrently
		if (event instanceof ConfigDataChangeEvent) {
			ConfigDataChangeEvent evt = (ConfigDataChangeEvent) event;
			long dumpTs = evt.lastModifiedTs;
			String dataId = evt.dataId;
			String group = evt.group;
			String tenant = evt.tenant;
			String tag = evt.tag;
			Collection<Member> ipList = memberManager.allMembers();
			
			// In fact, any type of queue here can be
			Queue<NotifySingleTask> httpQueue = new LinkedList<>();
			Queue<NotifySingleRpcTask> rpcQueue = new LinkedList<>();
			
			for (Member member : ipList) {
				if (!MemberUtil.isSupportedLongCon(member)) {
					httpQueue.add(new NotifySingleTask(dataId, group, tenant, tag, dumpTs, member.getAddress(),
							evt.isBeta));
				} else {
					rpcQueue.add(
							new NotifySingleRpcTask(dataId, group, tenant, tag, dumpTs, evt.isBeta, member));
				}
			}
			if (!httpQueue.isEmpty()) {
				ConfigExecutor.executeAsyncNotify(new AsyncTask(nacosAsyncRestTemplate, httpQueue));
			}
			if (!rpcQueue.isEmpty()) {
				ConfigExecutor.executeAsyncNotify(new AsyncRpcTask(rpcQueue));
			}
			
		}
	}
	
	@Override
	public Class<? extends Event> subscribeType() {
		return ConfigDataChangeEvent.class;
	}
});

最终在AsyncRpcTask中遍历所有服务端进行异步任务的处理,会先判断是否是当前节点本身,如果是当前节点自身则会直接执行dumpService.dump方法:

@Override
public void run() {
	while (!queue.isEmpty()) {
		NotifySingleRpcTask task = queue.poll();
		
		ConfigChangeClusterSyncRequest syncRequest = new ConfigChangeClusterSyncRequest();
		syncRequest.setDataId(task.getDataId());
		syncRequest.setGroup(task.getGroup());
		syncRequest.setBeta(task.isBeta);
		syncRequest.setLastModified(task.getLastModified());
		syncRequest.setTag(task.tag);
		syncRequest.setTenant(task.getTenant());
		Member member = task.member;
                //判断是否是自己
		if (memberManager.getSelf().equals(member)) {
			if (syncRequest.isBeta()) {
				dumpService.dump(syncRequest.getDataId(), syncRequest.getGroup(), syncRequest.getTenant(),
						syncRequest.getLastModified(), NetUtils.localIP(), true);
			} else {
				dumpService.dump(syncRequest.getDataId(), syncRequest.getGroup(), syncRequest.getTenant(),
						syncRequest.getTag(), syncRequest.getLastModified(), NetUtils.localIP());
			}
			continue;
		}
		//集群其他节点遍历 
		if (memberManager.hasMember(member.getAddress())) {
			// start the health check and there are ips that are not monitored, put them directly in the notification queue, otherwise notify
			boolean unHealthNeedDelay = memberManager.isUnHealth(member.getAddress());
			if (unHealthNeedDelay) {
				// target ip is unhealthy, then put it in the notification list
				ConfigTraceService.logNotifyEvent(task.getDataId(), task.getGroup(), task.getTenant(), null,
						task.getLastModified(), InetUtils.getSelfIP(), ConfigTraceService.NOTIFY_EVENT_UNHEALTH,
						0, member.getAddress());
				// get delay time and set fail count to the task
				asyncTaskExecute(task);
			} else {

				if (!MemberUtil.isSupportedLongCon(member)) {
					asyncTaskExecute(
							new NotifySingleTask(task.getDataId(), task.getGroup(), task.getTenant(), task.tag,
									task.getLastModified(), member.getAddress(), task.isBeta));
				} else {
					try {
						configClusterRpcClientProxy
								.syncConfigChange(member, syncRequest, new AsyncRpcNotifyCallBack(task));
					} catch (Exception e) {
						MetricsMonitor.getConfigNotifyException().increment();
						asyncTaskExecute(task);
					}
				}
			  
			}
		} else {
			//No nothig if  member has offline.
		}
		
	}
}

否则可以看到最后会发送一个RPC请求,入参为ConfigChangeClusterSyncRequest,其他服务

端收到请求后会基于对应的处理类ConfigChangeClusterSyncRequestHandler来进行数据的同

步更新。

@TpsControl(pointName = "ClusterConfigChangeNotify")
@Override
public ConfigChangeClusterSyncResponse handle(ConfigChangeClusterSyncRequest configChangeSyncRequest,
		RequestMeta meta) throws NacosException {
	
	if (configChangeSyncRequest.isBeta()) {
		dumpService.dump(configChangeSyncRequest.getDataId(), configChangeSyncRequest.getGroup(),
				configChangeSyncRequest.getTenant(), configChangeSyncRequest.getLastModified(), meta.getClientIp(),
				true);
	} else {
		dumpService.dump(configChangeSyncRequest.getDataId(), configChangeSyncRequest.getGroup(),
				configChangeSyncRequest.getTenant(), configChangeSyncRequest.getLastModified(), meta.getClientIp());
	}
	return new ConfigChangeClusterSyncResponse();
}
j

这里的dumpService.dump方法会去做具体的实现。此方法在后面会具体介绍。

客户端和服务端数据一致性

客户端与服务端的⼀致性的核心是通过 MD5 值是否⼀致,如果不⼀致就拉取最新值。如果通过

页面修改了配置,是如何做到通知各个客户端的?

1-我们先来看看客户端是如果获取配置信息的

content = configService.getConfig(dataId, group, 5000);

private String getConfigInner(String tenant, String dataId, String group, long timeoutMs) throws NacosException {
	group = blank2defaultGroup(group);
	ParamUtils.checkKeyParam(dataId, group);
	ConfigResponse cr = new ConfigResponse();
	
	cr.setDataId(dataId);
	cr.setTenant(tenant);
	cr.setGroup(group);
	
	// We first try to use local failover content if exists.
	// A config content for failover is not created by client program automatically,
	// but is maintained by user.
	// This is designed for certain scenario like client emergency reboot,
	// changing config needed in the same time, while nacos server is down.
        //读取本地文件
	String content = LocalConfigInfoProcessor.getFailover(worker.getAgentName(), dataId, group, tenant);
	if (content != null) {
		LOGGER.warn("[{}] [get-config] get failover ok, dataId={}, group={}, tenant={}, config={}",
				worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));
		cr.setContent(content);
		String encryptedDataKey = LocalEncryptedDataKeyProcessor
				.getEncryptDataKeyFailover(agent.getName(), dataId, group, tenant);
		cr.setEncryptedDataKey(encryptedDataKey);
		configFilterChainManager.doFilter(null, cr);
		content = cr.getContent();
		return content;
	}
	
        //远程请求服务端
	try {
		ConfigResponse response = worker.getServerConfig(dataId, group, tenant, timeoutMs, false);
		cr.setContent(response.getContent());
		cr.setEncryptedDataKey(response.getEncryptedDataKey());
		configFilterChainManager.doFilter(null, cr);
		content = cr.getContent();
		
		return content;
	} catch (NacosException ioe) {
		if (NacosException.NO_RIGHT == ioe.getErrCode()) {
			throw ioe;
		}
		LOGGER.warn("[{}] [get-config] get from server error, dataId={}, group={}, tenant={}, msg={}",
				worker.getAgentName(), dataId, group, tenant, ioe.toString());
	}

        //读取快照 
	content = LocalConfigInfoProcessor.getSnapshot(worker.getAgentName(), dataId, group, tenant);
	if (content != null) {
		LOGGER.warn("[{}] [get-config] get snapshot ok, dataId={}, group={}, tenant={}, config={}",
				worker.getAgentName(), dataId, group, tenant, ContentUtils.truncateContent(content));
	}
	cr.setContent(content);
	String encryptedDataKey = LocalEncryptedDataKeyProcessor
			.getEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant);
	cr.setEncryptedDataKey(encryptedDataKey);
	configFilterChainManager.doFilter(null, cr);
	content = cr.getContent();
	return content;
}

上面代码的主要实现流程如下:

1-先从本地文件中读取配置

public static String getFailover(String serverName, String dataId, String group, String tenant) {
	File localPath = getFailoverFile(serverName, dataId, group, tenant);
	if (!localPath.exists() || !localPath.isFile()) {
		return null;
	}
	
	try {
		return readFile(localPath);
	} catch (IOException ioe) {
		LOGGER.error("[" + serverName + "] get failover error, " + localPath, ioe);
		return null;
	}
}

2-本地文件没有读取到就会基于GRPC远程请求服务端,并且调用saveSnapshot方法写入快照

@Override
public ConfigResponse queryConfig(String dataId, String group, String tenant, long readTimeouts, boolean notify)
		throws NacosException {
	ConfigQueryRequest request = ConfigQueryRequest.build(dataId, group, tenant);
	request.putHeader(NOTIFY_HEADER, String.valueOf(notify));
	RpcClient rpcClient = getOneRunningClient();
	if (notify) {
		CacheData cacheData = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
		if (cacheData != null) {
			rpcClient = ensureRpcClient(String.valueOf(cacheData.getTaskId()));
		}
	}
	ConfigQueryResponse response = (ConfigQueryResponse) requestProxy(rpcClient, request, readTimeouts);
	
	ConfigResponse configResponse = new ConfigResponse();
	if (response.isSuccess()) {
		LocalConfigInfoProcessor.saveSnapshot(this.getName(), dataId, group, tenant, response.getContent());
		configResponse.setContent(response.getContent());
		String configType;
		if (StringUtils.isNotBlank(response.getContentType())) {
			configType = response.getContentType();
		} else {
			configType = ConfigType.TEXT.getType();
		}
		configResponse.setConfigType(configType);
		String encryptedDataKey = response.getEncryptedDataKey();
		LocalEncryptedDataKeyProcessor
				.saveEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant, encryptedDataKey);
		configResponse.setEncryptedDataKey(encryptedDataKey);
		return configResponse;
	} else if (response.getErrorCode() == ConfigQueryResponse.CONFIG_NOT_FOUND) {
		LocalConfigInfoProcessor.saveSnapshot(this.getName(), dataId, group, tenant, null);
		LocalEncryptedDataKeyProcessor.saveEncryptDataKeySnapshot(agent.getName(), dataId, group, tenant, null);
		return configResponse;
	} else if (response.getErrorCode() == ConfigQueryResponse.CONFIG_QUERY_CONFLICT) {
		LOGGER.error(
				"[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, "
						+ "tenant={}", this.getName(), dataId, group, tenant);
		throw new NacosException(NacosException.CONFLICT,
				"data being modified, dataId=" + dataId + ",group=" + group + ",tenant=" + tenant);
	} else {
		LOGGER.error("[{}] [sub-server-error]  dataId={}, group={}, tenant={}, code={}", this.getName(), dataId,
				group, tenant, response);
		throw new NacosException(response.getErrorCode(),
				"http error, code=" + response.getErrorCode() + ",msg=" + response.getMessage() + ",dataId="
						+ dataId + ",group=" + group + ",tenant=" + tenant);
		
	}
}

3-否则就会从快照中读取

content = LocalConfigInfoProcessor.getSnapshot(worker.getAgentName(), dataId, group, tenant);

2-如何实现客户端动态刷新的

Nacos 1.x的版本采用的是Http长连接的模式,每30s发⼀个心跳对比客户端配置MD5值是否跟

服务端保持⼀致,如果⼀致就 hold 住链接,进入等待期,在指定时间段内一致不返回,直到服

务端的配置发生了变动。如果30内没有变更就会发起返回给客户端。

Nacos 2.x 版本将长轮训的模式升级成了基于GRPC的长链接模式,在启动后客户端和服务端会

建立长连接,配置变更后服务端可以直接推送变更配置列表到客户端,然后客户端拉取配置更

新,通信效率大幅提升。

在前面的介绍中我们已经知道了在发布或者修改了配置后最终会调用到服务端下面的方法:

dumpService.dump(configChangeSyncRequest.getDataId(), configChangeSyncRequest.getGroup(),
                    configChangeSyncRequest.getTenant(), configChangeSyncRequest.getLastModified(), meta.getClientIp());

这个方法会执行到NacosDelayTaskExecuteEngine中的processTasks方法:

protected void processTasks() {
	Collection<Object> keys = getAllTaskKeys();
	for (Object taskKey : keys) {
		AbstractDelayTask task = removeTask(taskKey);
		if (null == task) {
			continue;
		}
		NacosTaskProcessor processor = getProcessor(taskKey);
		if (null == processor) {
			getEngineLog().error("processor not found for task, so discarded. " + task);
			continue;
		}
		try {
			// ReAdd task if process failed
			if (!processor.process(task)) {
				retryFailedTask(taskKey, task);
			}
		} catch (Throwable e) {
			getEngineLog().error("Nacos task execute error ", e);
			retryFailedTask(taskKey, task);
		}
	}
}

然后调用DumpProcessor中的process方法后继续调用DumpConfigHandler.configDump(build.build());最终会调用到ConfigCacheService中的dump方法

result = ConfigCacheService
			.dump(dataId, group, namespaceId, content, lastModified, type, encryptedDataKey);

这个方法所做的事情就是Save config file and update md5 value in cache. 保存配置到文件并且更新缓存中的md5值:

try {
	final String md5 = MD5Utils.md5Hex(content, Constants.ENCODE);
	if (lastModifiedTs < ConfigCacheService.getLastModifiedTs(groupKey)) {
		DUMP_LOG.warn("[dump-ignore] the content is old. groupKey={}, md5={}, lastModifiedOld={}, "
						+ "lastModifiedNew={}", groupKey, md5, ConfigCacheService.getLastModifiedTs(groupKey),
				lastModifiedTs);
		return true;
	}
	if (md5.equals(ConfigCacheService.getContentMd5(groupKey)) && DiskUtil.targetFile(dataId, group, tenant).exists()) {
		DUMP_LOG.warn("[dump-ignore] ignore to save cache file. groupKey={}, md5={}, lastModifiedOld={}, "
						+ "lastModifiedNew={}", groupKey, md5, ConfigCacheService.getLastModifiedTs(groupKey),
				lastModifiedTs);
	} else if (!PropertyUtil.isDirectRead()) {
		DiskUtil.saveToDisk(dataId, group, tenant, content);
	}
	updateMd5(groupKey, md5, lastModifiedTs, encryptedDataKey);
	return true;
} 

updateMd5方法实现:

public static void updateMd5(String groupKey, String md5, long lastModifiedTs, String encryptedDataKey) {
	CacheItem cache = makeSure(groupKey, encryptedDataKey, false);
	if (cache.md5 == null || !cache.md5.equals(md5)) {
		cache.md5 = md5;
		cache.lastModifiedTs = lastModifiedTs;
		NotifyCenter.publishEvent(new LocalDataChangeEvent(groupKey));
	}
}

如果服务端判断配置发生变更最终会通过RpcConfigChangeNotifier发送RPC请求通知客户端,

客户端在ClientWorker类中会处理此请求:

/*
 * Register Config Change /Config ReSync Handler
 */
rpcClientInner.registerServerRequestHandler((request) -> {
	if (request instanceof ConfigChangeNotifyRequest) {
		ConfigChangeNotifyRequest configChangeNotifyRequest = (ConfigChangeNotifyRequest) request;
		LOGGER.info("[{}] [server-push] config changed. dataId={}, group={},tenant={}",
				rpcClientInner.getName(), configChangeNotifyRequest.getDataId(),
				configChangeNotifyRequest.getGroup(), configChangeNotifyRequest.getTenant());
		String groupKey = GroupKey
				.getKeyTenant(configChangeNotifyRequest.getDataId(), configChangeNotifyRequest.getGroup(),
						configChangeNotifyRequest.getTenant());
		
		CacheData cacheData = cacheMap.get().get(groupKey);
		if (cacheData != null) {
			synchronized (cacheData) {
				cacheData.getLastModifiedTs().set(System.currentTimeMillis());
				cacheData.setSyncWithServer(false);
				notifyListenConfig();
			}
			
		}
		return new ConfigChangeNotifyResponse();
	}
	return null;
});

在其notifyListenConfig方法最终的实现如下:

private void refreshContentAndCheck(CacheData cacheData, boolean notify) {
	try {
		ConfigResponse response = getServerConfig(cacheData.dataId, cacheData.group, cacheData.tenant, 3000L,
				notify);
		cacheData.setEncryptedDataKey(response.getEncryptedDataKey());
		cacheData.setContent(response.getContent());
		if (null != response.getConfigType()) {
			cacheData.setType(response.getConfigType());
		}
		if (notify) {
			LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
					agent.getName(), cacheData.dataId, cacheData.group, cacheData.tenant, cacheData.getMd5(),
					ContentUtils.truncateContent(response.getContent()), response.getConfigType());
		}
		cacheData.checkListenerMd5();
	} catch (Exception e) {
		LOGGER.error("refresh content and check md5 fail ,dataId={},group={},tenant={} ", cacheData.dataId,
				cacheData.group, cacheData.tenant, e);
	}
}

void checkListenerMd5() {
	for (ManagerListenerWrap wrap : listeners) {
		if (!md5.equals(wrap.lastCallMd5)) {
			safeNotifyListener(dataId, group, content, type, md5, encryptedDataKey, wrap);
		}
	}
}

上面的代码中会先通过getServerConfig方法向服务端发送RPC请求来获取最新的配置信息。服

务端通过ConfigQueryRequestHandler来进行处理客户端的请求,从DB中获取配置信息,(具体

的实现可以自己去ConfigQueryRequestHandler中查看)。然后设置到cacheData中,并调用其

checkListenerMd5方法。最终在safeNotifyListener方法中实现了客户端的刷新,调用了

listener.receiveConfigInfo(contentTmp); 也就是我们demo中的回调方法。

Runnable job = () -> {
	long start = System.currentTimeMillis();
	ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();
	ClassLoader appClassLoader = listener.getClass().getClassLoader();
	try {
		if (listener instanceof AbstractSharedListener) {
			AbstractSharedListener adapter = (AbstractSharedListener) listener;
			adapter.fillContext(dataId, group);
			LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);
		}
		// Before executing the callback, set the thread classloader to the classloader of
		// the specific webapp to avoid exceptions or misuses when calling the spi interface in
		// the callback method (this problem occurs only in multi-application deployment).
		Thread.currentThread().setContextClassLoader(appClassLoader);
		
		ConfigResponse cr = new ConfigResponse();
		cr.setDataId(dataId);
		cr.setGroup(group);
		cr.setContent(content);
		cr.setEncryptedDataKey(encryptedDataKey);
		configFilterChainManager.doFilter(null, cr);
		String contentTmp = cr.getContent();
		listenerWrap.inNotifying = true;
		listener.receiveConfigInfo(contentTmp);
		// compare lastContent and content
		if (listener instanceof AbstractConfigChangeListener) {
			Map<String, ConfigChangeItem> data = ConfigChangeHandler.getInstance()
					.parseChangeData(listenerWrap.lastContent, contentTmp, type);
			ConfigChangeEvent event = new ConfigChangeEvent(data);
			((AbstractConfigChangeListener) listener).receiveConfigChange(event);
			listenerWrap.lastContent = contentTmp;
		}
		
		listenerWrap.lastCallMd5 = md5;
		LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ,cost={} millis.", name, dataId,
				group, md5, listener, (System.currentTimeMillis() - start));
	} catch (NacosException ex) {
		LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}", name,
				dataId, group, md5, listener, ex.getErrCode(), ex.getErrMsg());
	} catch (Throwable t) {
		LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={}", name, dataId, group, md5,
				listener, t);
	} finally {
		listenerWrap.inNotifying = false;
		Thread.currentThread().setContextClassLoader(myClassLoader);
	}
};

总结

到此,已经大致了解了Nacos中关于注册中心的一些设计,后续需要继续在实战中对其进行更深入的了解。