入口
//NacosConfigService
onfigService 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("recieve1:" + configInfo);
}
@Override
public Executor getExecutor() {
return null;
}
});
进入nacosConfigService构造方法
public NacosConfigService(Properties properties) throws NacosException {
this.initNamespace(properties);
//监控统计代理
this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
this.agent.start();
this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}
初始化了一个http代理和worker
在work里进行本地和远程配置的检查
public ClientWorker(final HttpAgent agent, ConfigFilterChainManager configFilterChainManager, Properties properties) {
this.agent = agent;
this.configFilterChainManager = configFilterChainManager;
this.init(properties);
this.executor = Executors.newScheduledThreadPool(1, new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
t.setDaemon(true);
return t;
}
});
//长轮训初始化
this.executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
t.setDaemon(true);
return t;
}
});
//每10毫秒检测配置
this.executor.scheduleWithFixedDelay(new Runnable() {
public void run() {
try {
ClientWorker.this.checkConfigInfo();
} catch (Throwable var2) {
ClientWorker.LOGGER.error("[" + agent.getName() + "] [sub-check] rotate check error", var2);
}
}
}, 1L, 10L, TimeUnit.MILLISECONDS);
}
分块任务,向上取整,进行长轮训的检测本地缓存配置
public void checkConfigInfo() {
int listenerSize = ((Map)this.cacheMap.get()).size();
int longingTaskCount = (int)Math.ceil((double)listenerSize / ParamUtil.getPerTaskConfigSize());
if ((double)longingTaskCount > this.currentLongingTaskCount) {
for(int i = (int)this.currentLongingTaskCount; i < longingTaskCount; ++i) {
this.executorService.execute(new ClientWorker.LongPollingRunnable(i));
}
this.currentLongingTaskCount = (double)longingTaskCount;
}
}
ClientWorker.this.checkLocalConfig(cacheData);
if (cacheData.isUseLocalConfigInfo()) {
cacheData.checkListenerMd5();//如有有不一样回调入口方法的监听
}
private void checkLocalConfig(CacheData cacheData) {
String dataId = cacheData.dataId;
String group = cacheData.group;
String tenant = cacheData.tenant;
File path = LocalConfigInfoProcessor.getFailoverFile(this.agent.getName(), dataId, group, tenant);
String content;
String md5;
//将本地文件读取到本地配置
if (!cacheData.isUseLocalConfigInfo() && path.exists()) {
content = LocalConfigInfoProcessor.getFailover(this.agent.getName(), dataId, group, tenant);
md5 = MD5.getInstance().getMD5String(content);
cacheData.setUseLocalConfigInfo(true);
cacheData.setLocalConfigInfoVersion(path.lastModified());
cacheData.setContent(content);
LOGGER.warn("[{}] [failover-change] failover file created. dataId={}, group={}, tenant={}, md5={}, content={}", new Object[]{this.agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content)});
// 有->没有不通知业务监听器,从server拿到配置后通知
} else if (cacheData.isUseLocalConfigInfo() && !path.exists()) {
cacheData.setUseLocalConfigInfo(false);
LOGGER.warn("[{}] [failover-change] failover file deleted. dataId={}, group={}, tenant={}", new Object[]{this.agent.getName(), dataId, group, tenant});
} else {
// 有变更
if (cacheData.isUseLocalConfigInfo() && path.exists() && cacheData.getLocalConfigInfoVersion() != path.lastModified()) {
content = LocalConfigInfoProcessor.getFailover(this.agent.getName(), dataId, group, tenant);
md5 = MD5.getInstance().getMD5String(content);
cacheData.setUseLocalConfigInfo(true);
cacheData.setLocalConfigInfoVersion(path.lastModified());
cacheData.setContent(content);
LOGGER.warn("[{}] [failover-change] failover file changed. dataId={}, group={}, tenant={}, md5={}, content={}", new Object[]{this.agent.getName(), dataId, group, tenant, md5, ContentUtils.truncateContent(content)});
}
}
}
远程配置检查
//从server里获取值变化了的DataID列表,返回的对象只有dataId和groupId是有效的
List<String> changedGroupKeys = ClientWorker.this.checkUpdateDataIds(cacheDatas, inInitializingCacheList);
List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {
List<String> params = new ArrayList(2);
params.add("Listening-Configs");
params.add(probeUpdateString);
List<String> headers = new ArrayList(2);
headers.add("Long-Pulling-Timeout");
headers.add("" + this.timeout);
if (isInitializingCacheList) {
headers.add("Long-Pulling-Timeout-No-Hangup");
headers.add("true");
}
if (StringUtils.isBlank(probeUpdateString)) {
return Collections.emptyList();
} else {
try {
long readTimeoutMs = this.timeout + (long)Math.round((float)(this.timeout >> 1));
//客户端长轮训的时间间隔30s.在配置没有发生变化的情况下,客户端会等29.5s以上如果在配置变更的情况下,由于客户端基于长轮训的连接保持,所以返回的时间会非常的短
HttpResult result = this.agent.httpPost("/v1/cs/configs/listener", headers, params, this.agent.getEncode(), readTimeoutMs);
if (200 == result.code) {
this.setHealthServer(true);
return this.parseUpdateDataIdResponse(result.content);
}
this.setHealthServer(false);
LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", this.agent.getName(), result.code);
} catch (IOException var8) {
this.setHealthServer(false);
LOGGER.error("[" + this.agent.getName() + "] [check-update] get changed dataId exception", var8);
throw var8;
}
return Collections.emptyList();
}
}
服务端是如何处理客户端的长轮训
ConfigController
@RequestMapping(value = "/listener", method = RequestMethod.POST)
public void listener(HttpServletRequest request, HttpServletResponse
response)
throws ServletException, IOException {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
String probeModify = request.getParameter("Listening-Configs");
if (StringUtils.isBlank(probeModify)) {
throw new IllegalArgumentException("invalid probeModify");
}
probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);
Map<String, String> clientMd5Map;
try {
clientMd5Map = MD5Util.getClientMd5Map(probeModify);
} catch (Throwable e) {
throw new IllegalArgumentException("invalid probeModify");
}
// do long-polling
inner.doPollingConfig(request, response, clientMd5Map,
probeModify.length());
}
简单总结一下刚刚分析的整个过程。
- 客户端发起长轮训请求
- 服务端收到请求以后,先比较服务端缓存中的数据是否相同,如果不同,则直接返回
- 如果相同,则通过schedule延迟29.5s之后再执行比较
- 为了保证当服务端在29.5s之内发生数据变化能够及时通知给客户端,服务端采用事件订阅的方式 来监听服务端本地数据变化的事件,一旦收到事件,则触发DataChangeTask的通知,并且遍历 allStubs队列中的ClientLongPolling,把结果写回到客户端,就完成了一次数据的推送
- 如果 DataChangeTask 任务完成了数据的 “推送” 之后,ClientLongPolling 中的调度任务又开始执 行了怎么办呢? 很简单,只要在进行 “推送” 操作之前,先将原来等待执行的调度任务取消掉就可以了,这样就防 止了推送操作写完响应数据之后,调度任务又去写响应数据,这时肯定会报错的。所以,在 ClientLongPolling方法中,最开始的一个步骤就是删除订阅事件