回顾上一篇: admin 启动之后,会每隔5分钟查询一次数据库,把数据刷新到缓存
soul-bootstrap端
-
根据上一节yml文件配置的http同步配置方式,实例化
httpSyncDataService
类。@Configuration @ConditionalOnClass(HttpSyncDataService.class) @ConditionalOnProperty(prefix = "soul.sync.http", name = "url") @Slf4j public class HttpSyncDataConfiguration { @Bean public SyncDataService httpSyncDataService(final ObjectProvider<HttpConfig> httpConfig, final ObjectProvider<PluginDataSubscriber> pluginSubscriber, final ObjectProvider<List<MetaDataSubscriber>> metaSubscribers, final ObjectProvider<List<AuthDataSubscriber>> authSubscribers) { log.info("you use http long pull sync soul data"); return new HttpSyncDataService(Objects.requireNonNull(httpConfig.getIfAvailable()), Objects.requireNonNull(pluginSubscriber.getIfAvailable()), metaSubscribers.getIfAvailable(Collections::emptyList), authSubscribers.getIfAvailable(Collections::emptyList)); } @Bean @ConfigurationProperties(prefix = "soul.sync.http") public HttpConfig httpConfig() { return new HttpConfig(); } }
2021-02-01 17:22:04.279 INFO 17272 --- [ main] .s.s.b.s.s.d.h.HttpSyncDataConfiguration : you use http long pull sync soul data
-
bootstrap 启动之后,会先从 admin 拉取数据,看
HttpSyncDataService
的start 方法 fetchGroupConfig(),启用一个存活时间60秒的线程去更新缓存。private void start() { // It could be initialized multiple times, so you need to control that. if (RUNNING.compareAndSet(false, true)) { // fetch all group configs. this.fetchGroupConfig(ConfigGroupEnum.values()); int threadSize = serverList.size(); this.executor = new ThreadPoolExecutor(threadSize, threadSize, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), SoulThreadFactory.create("http-long-polling", true)); // start long polling, each server creates a thread to listen for changes. this.serverList.forEach(server -> this.executor.execute(new HttpLongPollingTask(server))); } else { log.info("soul http long polling was started, executor=[{}]", executor); } }
-
httpClient请求数据,更新本地缓存
private void doFetchGroupConfig(final String server, final ConfigGroupEnum... groups) { StringBuilder params = new StringBuilder(); for (ConfigGroupEnum groupKey : groups) { params.append("groupKeys").append("=").append(groupKey.name()).append("&"); } String url = server + "/configs/fetch?" + StringUtils.removeEnd(params.toString(), "&"); log.info("request configs: [{}]", url); String json = null; try { json = this.httpClient.getForObject(url, String.class); } catch (RestClientException e) { String message = String.format("fetch config fail from server[%s], %s", url, e.getMessage()); log.warn(message); throw new SoulException(message, e); } // update local cache boolean updated = this.updateCacheWithJson(json); if (updated) { log.info("get latest configs: [{}]", json); return; } // not updated. it is likely that the current config server has not been updated yet. wait a moment. log.info("The config of the server[{}] has not been updated or is out of date. Wait for 30s to listen for changes again.", server); ThreadUtils.sleep(TimeUnit.SECONDS, 30); }
-
看到控制台打印的日志:同步的数据内容
2021-02-01 17:37:02.231 INFO 17272 --- [ main] o.d.s.s.data.http.HttpSyncDataService : get latest configs: [{"code":200,"message":"success","data":{"META_DATA":{"md5":"f8e578e88c1fd20821ba26fd5647e406","lastModifyTime":1612172026454,"data":[{"id":"1354786856173363200","appName":"sofa","contextPath":null,"path":"/sofa/insert","rpcType":"sofa","serviceName":"org.dromara.soul.examples.sofa.api.service.SofaSingleParamService","methodName":"insert","parameterTypes":"org.dromara.soul.examples.sofa.api.entity.SofaSimpleTypeBean","rpcExt":"{\"loadbalance\":\"hash\",\"retries\":3,\"timeout\":-1}","enabled":true},{"id":"1354786857481986048","appName":"sofa","contextPath":null,"path":"/sofa/findById","rpcType":"sofa","serviceName":"org.dromara.soul.examples.sofa.api.service.SofaSingleParamService","methodName":"findById","parameterTypes":"java.lang.String","rpcExt":"{\"loadbalance\":\"hash\",\"retries\":3,\"timeout\":-1}","enabled":true},{"id":"1354786858190823424","appName":"sofa","contextPa。。。。。。。。
-
接着看到 进入doLongPolling(final String server) 方法,每隔 60s 调用一次 /configs/listener ,把当前网关里缓存的数据和最后更新的时间戳发给 admin
//向 admin 发送请求
String json = this.httpClient.postForEntity(listenerUrl, httpEntity, String.class).getBody();
-
admin 收到请求后,会去判断数据的MD5,数据有没有更新,还有最后更新的时间戳,检查数据是否需要更新,有数据更新的话,会返回更新数据的 group 给 bootstrap。
public void doLongPolling(final HttpServletRequest request, final HttpServletResponse response) { // compare group md5 List<ConfigGroupEnum> changedGroup = compareChangedGroup(request); String clientIp = getRemoteIp(request); // response immediately. if (CollectionUtils.isNotEmpty(changedGroup)) { this.generateResponse(response, changedGroup); log.info("send response with the changed group, ip={}, group={}", clientIp, changedGroup); return; } // ...... }
private boolean checkCacheDelayAndUpdate(final ConfigDataCache serverCache, final String clientMd5, final long clientModifyTime) { // iMD5相等,说明数据不需要更新 if (StringUtils.equals(clientMd5, serverCache.getMd5())) { return false; } // // MD5不相等,admin 缓存里的最后更新时间戳比 bootstrap 端的最后更新时间戳大,说明数据过期了 long lastModifyTime = serverCache.getLastModifyTime(); if (lastModifyTime >= clientModifyTime) { // the client's config is out of date. return true; } // admin 缓存里的最后更新时间戳比 bootstrap 端的最后更新时间戳小, 说明本地缓存需要更新. // // 考虑到并发问题,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(); } } // not locked, the client need to be updated. return true; }
-
bootstrap 收到响应后,会判断有没有数据更新,如果有数据更新,就调用 admin 的 /configs/fetch 获取数据
try { String json = this.httpClient.postForEntity(listenerUrl, httpEntity, String.class).getBody(); log.debug("listener result: [{}]", json); groupJson = GSON.fromJson(json, JsonObject.class).getAsJsonArray("data"); } catch (RestClientException e) { String message = String.format("listener configs fail, server:[%s], %s", server, e.getMessage()); throw new SoulException(message, e); } // 有数据更新时 if (groupJson != null) { // fetch group configuration async. ConfigGroupEnum[] changedGroups = GSON.fromJson(groupJson, ConfigGroupEnum[].class); if (ArrayUtils.isNotEmpty(changedGroups)) { log.info("Group config changed: {}", Arrays.toString(changedGroups)); this.doFetchGroupConfig(server, changedGroups); } }
private void doFetchGroupConfig(final String server, final ConfigGroupEnum... groups) { ...... String url = server + "/configs/fetch?" + StringUtils.removeEnd(params.toString(), "&"); ...... try { json = this.httpClient.getForObject(url, String.class); } catch (RestClientException e) { String message = String.format("fetch config fail from server[%s], %s", url, e.getMessage()); log.warn(message); throw new SoulException(message, e); } // update local cache boolean updated = this.updateCacheWithJson(json); if (updated) { log.info("get latest configs: [{}]", json); return; }
-
从缓存里获取配置
ConfigController
#fetchConfigs
-
ConfigController
#listener
如果配置数据更改,则会立即响应更改的信息。否则,客户端的请求线程将被阻塞,直到数据发生变化或达到指定的超时时间。