soul源码解读(八)
数据同步之http长轮询-bootstrap端
我们梳理下昨天调试的 http 长轮询同步数据的流程
首先, admin 启动之后,会每隔5分钟查询一次数据库,把数据刷新到缓存
bootstrap 启动之后,会先从 admin 拉取数据
// HttpSyncDataService.java
// fetch all group configs.
this.fetchGroupConfig(ConfigGroupEnum.values());
然后 bootstrap 会每隔 60s 调用一次 /configs/listener ,把当前网关里缓存的数据和最后更新的时间戳发给 admin
//当前网关里缓存的数据和最后更新的时间戳
for (ConfigGroupEnum group : ConfigGroupEnum.values()) {
ConfigData<?> cacheConfig = factory.cacheConfigData(group);
String value = String.join(",", cacheConfig.getMd5(), String.valueOf(cacheConfig.getLastModifyTime()));
params.put(group.name(), Lists.newArrayList(value));
}
...
String listenerUrl = server + "/configs/listener";
...
//向 admin 发送请求
String json = this.httpClient.postForEntity(listenerUrl, httpEntity, String.class).getBody();
admin 收到请求后,会去判断数据的MD5,数据有没有更新,还有最后更新的时间戳
// HttpLongPollingDataChangedListener.java
private List<ConfigGroupEnum> compareChangedGroup(final HttpServletRequest request) {
...
// 检查有没有数据更新
if (this.checkCacheDelayAndUpdate(serverCache, clientMd5, clientModifyTime)) {
changedGroup.add(group);
}
}
return changedGroup;
}
如果有数据更新,会先获取锁,然后判断两次从本地缓存获取的数据是否一致,一致的话,直接再从数据库拉取一次数据,然后返回数据有更新,不一致的话,再判断两个对象的MD5值是否相同,不相同,说明有更新。
// MD5相等,说明数据没有更新
if (StringUtils.equals(clientMd5, serverCache.getMd5())) {
return false;
}
long lastModifyTime = serverCache.getLastModifyTime();
if (lastModifyTime >= clientModifyTime) {
// MD5不相等,admin 缓存里的最后更新时间戳比 bootstrap 端的最后更新时间戳大,说明数据过期了
return true;
}
// 考虑到并发问题,admin需要加锁,否则,可能会导致soul-web的请求同时更新缓存,从而导致数据库压力过大
boolean locked = false;
try {
// 5s内尝试去获取锁,如果没获取到就抛异常
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) {
//在并发的时候,数据有变动,两次从本地缓存获取的数据不一致,看下两个对象的MD5值是否相同,不相同,说明有更新。
return !StringUtils.equals(clientMd5, latest.getMd5());
}
// 为了保险起见,再从数据库拉取一次数据
this.refreshLocalCache();
latest = CACHE.get(serverCache.getGroup());
return !StringUtils.equals(clientMd5, latest.getMd5());
} finally {
LOCK.unlock();
}
}
有数据更新的话,会返回更新数据的 group 给 bootstrap
if (CollectionUtils.isNotEmpty(changedGroup)) {
this.generateResponse(response, changedGroup);
log.info("send response with the changed group, ip={}, group={}", clientIp, changedGroup);
return;
}
bootstrap 收到响应后,会判断有没有数据更新,如果有数据更新,就调用 admin 的 /configs/fetch 获取数据
//获取 /configs/listener 接口的返回数据
groupJson = GSON.fromJson(json, JsonObject.class).getAsJsonArray("data");
// 判断有没有数据更新
if (groupJson != null) {
// fetch group configuration async.
ConfigGroupEnum[] changedGroups = GSON.fromJson(groupJson, ConfigGroupEnum[].class);
if (ArrayUtils.isNotEmpty(changedGroups)) {
//有数据更新,就调用 admin 的 /configs/fetch
log.info("Group config changed: {}", Arrays.toString(changedGroups));
this.doFetchGroupConfig(server, changedGroups);
}
}
再回到 admin ,/configs/listener 判断如果没有数据更新,会把请求放到阻塞队列里,每隔60s去队列里消费消息,里面又会去判断有没有数据更新
public void run() {
this.asyncTimeoutFuture = scheduler.schedule(() -> {
clients.remove(LongPollingClient.this);
List<ConfigGroupEnum> changedGroups = compareChangedGroup((HttpServletRequest) asyncContext.getRequest());
sendResponse(changedGroups);
}, timeoutTime, TimeUnit.MILLISECONDS);
clients.add(this);
}
整体流程图
总结
通过昨天和今天的梳理,理解清楚了 admin 和 bootstrap 两端是如何通过 http 长轮询保持数据同步的。简单的说,就是bootstrap 会调用一个 listener 接口,admin 会返回有没有数据更新,有的话,bootstrap 就再调用 fetch 接口获取最新数据。