分析前准备
环境准备
- soul-admin开启http同步配置
soul:
database:
dialect: mysql
init_script: "META-INF/schema.sql"
init_enable: true
sync:
# websocket:
# enabled: true
# zookeeper:
# url: localhost:2181
# sessionTimeout: 5000
# connectionTimeout: 2000
http:
enabled: true
- soul-bootstrap 开启http同步配置
soul :
file:
enabled: true
corss:
enabled: true
dubbo :
parameter: multi
sync:
# websocket :
# urls: ws://localhost:9095/websocket
# zookeeper:
# url: localhost:2181
# sessionTimeout: 5000
# connectionTimeout: 2000
http:
url : http://localhost:9095
数据同步过程
soul-bootstrap启动后,会主动向soul-admin发起一次获取全量配置的请求,并将结果缓存到本地,同时开启线程做以下功能:
- 首先向soul-admin发起获取分组信息是否变更的请求,soul-admin接收到该请求后,将其请求保存到阻塞队列中,并在60s后出队列执行分组数据是否变更的判断,无论是否变化,都会给soul-boostrap发回响应信息,soul-bootstrap根据结果来判定是否再次请求获取便发生变化的配置信息
- 功能1在线程内为死循环操作,由于发起的请求会被soul-admin阻塞60s,因此在这60s期间,如果有数据发生变更(spring事件机制通知数据变更),会立刻响应soul-bootstrap,而不一直等待到60s再响应
- 需要注意的是如果soul-bootstrap内执行线程获取了变更配置信息,会暂停30s后,会再次发出请求,如果没有变更配置信息,会立马发起下次请求
通过以上办法来实现http长轮询机制,具有以下优点: - 减少请求次数
- 数据变更能及时响应客户端.
源码分析
- soul-boostrap启动后的处理关键源码(位于HttpSyncDataService类中):
图中第一处标红为首次获取全量配置的请求
图中第二处标红为线程内的长轮询操作,具体关键源码如下:
可以看到是死循环不断发送请求操作,其中轮询方法关键代码如下:
private void doLongPolling(final String server) {
//代码省略
String listenerUrl = server + "/configs/listener";
log.debug("request listener configs: [{}]", listenerUrl);
JsonArray groupJson = null;
try {
String json = this.httpClient.postForEntity(listenerUrl, httpEntity, String.class).getBody();//该处为分组信息是否变更的请求
groupJson = GSON.fromJson(json, JsonObject.class).getAsJsonArray("data");
} catch (RestClientException e) {
.....
}
if (groupJson != null) {
ConfigGroupEnum[] changedGroups = GSON.fromJson(groupJson, ConfigGroupEnum[].class);
if (ArrayUtils.isNotEmpty(changedGroups)) {
this.doFetchGroupConfig(server, changedGroups);//该处为获取变更的配置信息
}
}
}
获取配置信息的方法fetchGroupConfig,内部获取成功后,会暂停30s,代码不再展示
2. soul-admin接收到请求后,接受处理源码为:
调用最关键的源码:
讲请求变为异步并放到阻塞队列中,设置定时事件为60s,其中LongPollingClient为内部类,执行的方法为:
60s后出队列,执行比较配置信息是否发生变更操作,并响应给客户端,客户端根据是否变更来判断是否发起获取配置信息,soul-bootrap已分析,soul-admin的处理代码如下:
具体处理过程不再分析
- soul-admin在界面进行相关数据变更时,简单分析下流转(基于事件机制不再分析),看关键代码:
接收到事件通知后,通过事件分发器调用以上代码,关键方法为DataChangeTask也是内部方法,关键代码为
可以看到直接告知连接的客户端数据已发生变更,并没有2中的先判断数据是否已变更,再响应,因为此事件是已明确变更了,不用再判断 - 最后分析下判断分组信息变更的逻辑:
private boolean checkCacheDelayAndUpdate(final ConfigDataCache serverCache, final String clientMd5, final long clientModifyTime) {
// 判断客户端传来的md5只要与目前缓存中是否一致
if (StringUtils.equals(clientMd5, serverCache.getMd5())) {
return false;
}
//判断缓存最后修改时间是否大于客户端修改时间
if (lastModifyTime >= clientModifyTime) {
// the client's config is out of date.
return true;
}
//如果最后修改时间小于客户端的时间,要进行加锁,因为要多线程环境下要处理数据库刷新操作
boolean locked = false;
try {
locked = LOCK.tryLock(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return true;
}
if (locked) {
try {
ConfigDataCache latest = CACHE.get(serverCache.getGroup());
if (latest != serverCache) {
//由于admin缓存可能会被更新,两次缓存可能会不一致
return !StringUtils.equals(clientMd5, latest.getMd5());
}
// load cache from db.
this.refreshLocalCache();
latest = CACHE.get(serverCache.getGroup());
return !StringUtils.equals(clientMd5, latest.getMd5());
} finally {
LOCK.unlock();
}
} return true;
}
关于为什么要进行区分修改时间的判断,猜测可能是因为事件机制的原因,缓存和数据库操作不在同一事物中导致数据库更新了,但缓存未更新,客户端有可能会出现更新时间比服务器端的缓存中的时间要新,这种情况都可能要根据数据库刷新缓存了。