概念
zookeeper、websocket 数据同步的机制比较简单,而 http 同步会相对复杂一些。Soul 借鉴了 Apollo、Nacos 的设计思想,取其精华,自己实现了 http 长轮询数据同步功能。注意,这里并非传统的 ajax 长轮询!
服务启动
-
启用http同步配置方式,修改admin的application.yml
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 # nacos: # url: localhost:8848 # namespace: soul # acm: # enabled: false # endpoint: acm.aliyun.com # namespace: # accessKey: # secretKey:
-
修改 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 # nacos: # url: localhost:8848 # namespace: soul # acm: # enabled: false # endpoint: acm.aliyun.com # namespace: # accessKey: # secretKey:
-
bootstrap的pom.xml引入http依赖:
<!--soul data sync start use http--> <dependency> <groupId>org.dromara</groupId> <artifactId>soul-spring-boot-starter-sync-data-http</artifactId> <version>${project.version}</version> </dependency>
soul-admin端
-
http长轮询同步数据方式的初始化同websocket、zk和nacos一样,都是在DataSyncConfiguration启动的时候,实例化一个监听类。
-
程序启动的时候因为加了这个注解
@Configuration
,会去加载这个beanDataSyncConfiguration
,因为刚才我们在yml启用了soul.sync.http.enabled = true,满足了条件注解@ConditionalOnProperty
,所以会初始化一个HttpLongPollingListener
@Configuration public class DataSyncConfiguration { /** * http long polling. */ @Configuration @ConditionalOnProperty(name = "soul.sync.http.enabled", havingValue = "true") @EnableConfigurationProperties(HttpSyncProperties.class) static class HttpLongPollingListener { @Bean @ConditionalOnMissingBean(HttpLongPollingDataChangedListener.class) public HttpLongPollingDataChangedListener httpLongPollingDataChangedListener(final HttpSyncProperties httpSyncProperties) { return new HttpLongPollingDataChangedListener(httpSyncProperties); } }
-
admin启动的时候,控制台可以看到打印日志:
2021-01-30 22:06:34.378 INFO 19164 --- [ main] a.l.h.HttpLongPollingDataChangedListener : http sync strategy refresh interval: 300000ms ...... 2021-01-30 22:06:34.378 INFO 19164 --- [ main] a.l.h.HttpLongPollingDataChangedListener : http sync strategy refresh interval: 300000ms
-
根据日志打印的地方debug到http长轮询监听类
HttpLongPollingDataChangedListener
。scheduler.scheduleWithFixedDelay,设置一个延迟执行的线程池,刷新本地缓存*protected void afterInitialize() { long syncInterval = httpSyncProperties.getRefreshInterval().toMillis(); // Periodically check the data for changes and update the cache scheduler.scheduleWithFixedDelay(() -> { log.info("http sync strategy refresh config start."); try { this.refreshLocalCache(); log.info("http sync strategy refresh config success."); } catch (Exception e) { log.error("http sync strategy refresh config error!", e); } }, syncInterval, syncInterval, TimeUnit.MILLISECONDS); log.info("http sync strategy refresh interval: {}ms", syncInterval); }
-
看到打印了同步开始和同步成功日志,时间刚好对应上面的5分钟(300000ms),判断admin每隔5分钟会从数据库刷新一次配置。
2021-01-30 22:11:34.383 INFO 19164 --- [-long-polling-2] a.l.h.HttpLongPollingDataChangedListener : http sync strategy refresh config start. 2021-01-30 22:11:34.386 INFO 19164 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[APP_AUTH], old: {group='APP_AUTH', md5='d751713988987e9331980363e24189ce', lastModifyTime=1612015594064}, updated: {group='APP_AUTH', md5='d751713988987e9331980363e24189ce', lastModifyTime=1612015894386} 2021-01-30 22:11:34.390 INFO 19164 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[PLUGIN], old: {group='PLUGIN', md5='0b72170e66c6ae436cef1f214504936a', lastModifyTime=1612015594089}, updated: {group='PLUGIN', md5='0b72170e66c6ae436cef1f214504936a', lastModifyTime=1612015894390} 2021-01-30 22:11:34.504 INFO 19164 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[RULE], old: {group='RULE', md5='d64c9964f7837662af61c5e1981e3c41', lastModifyTime=1612015594333}, updated: {group='RULE', md5='d64c9964f7837662af61c5e1981e3c41', lastModifyTime=1612015894504} 2021-01-30 22:11:34.512 INFO 19164 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[SELECTOR], old: {group='SELECTOR', md5='4231dce5f8ece7b7956c25e9db088e44', lastModifyTime=1612015594354}, updated: {group='SELECTOR', md5='4231dce5f8ece7b7956c25e9db088e44', lastModifyTime=1612015894512} 2021-01-30 22:11:34.515 INFO 19164 --- [-long-polling-2] o.d.s.a.l.AbstractDataChangedListener : update config cache[META_DATA], old: {group='META_DATA', md5='f8e578e88c1fd20821ba26fd5647e406', lastModifyTime=1612015594377}, updated: {group='META_DATA', md5='f8e578e88c1fd20821ba26fd5647e406', lastModifyTime=1612015894515} 2021-01-30 22:11:34.515 INFO 19164 --- [-long-polling-2] a.l.h.HttpLongPollingDataChangedListener : http sync strategy refresh config success.
-
刷新5种数据的本地缓存,权限、插件、规则、选择器和元数据。
private void refreshLocalCache() { this.updateAppAuthCache(); this.updatePluginCache(); this.updateRuleCache(); this.updateSelectorCache(); this.updateMetaDataCache(); }
-
HttpLongPollingDataChangedListener
继承AbstractDataChangedListener
,afterPropertiesSet方法调用了上面说的 afterInitialize方法,AbstractDataChangedListener
又实现了DataChangedListener
InitializingBean
-
当数据发生更改时(比如选择器或者插件),将创建线程以异步通知客户端,看内部类
DataChangeTask
实现了Runnable
。class DataChangeTask implements Runnable { /** * The Group where the data has changed. */ private final ConfigGroupEnum groupKey; /** * The Change time. */ private final long changeTime = System.currentTimeMillis(); /** * Instantiates a new Data change task. * * @param groupKey the group key */ DataChangeTask(final ConfigGroupEnum groupKey) { this.groupKey = groupKey; } @Override public void run() { for (Iterator<LongPollingClient> iter = clients.iterator(); iter.hasNext();) { LongPollingClient client = iter.next(); iter.remove(); client.sendResponse(Collections.singletonList(groupKey)); log.info("send response with the changed group,ip={}, group={}, changeTime={}", client.ip, groupKey, changeTime); } } }
-
核心方法
run()
里又调用了一个内部类LongPollingClient
的方法 sendResponse(),这个run() 创建了一个延迟线程池的任务方法。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); }
-
继续debug 发现一个核心方法,会去判断数据的MD5,数据有没有更新,还有最后更新的时间戳,如果需要数据更新,返回数据。
private List<ConfigGroupEnum> compareChangedGroup(final HttpServletRequest request) { List<ConfigGroupEnum> changedGroup = new ArrayList<>(ConfigGroupEnum.values().length); for (ConfigGroupEnum group : ConfigGroupEnum.values()) { // md5,lastModifyTime String[] params = StringUtils.split(request.getParameter(group.name()), ','); if (params == null || params.length != 2) { throw new SoulException("group param invalid:" + request.getParameter(group.name())); } String clientMd5 = params[0]; long clientModifyTime = NumberUtils.toLong(params[1]); ConfigDataCache serverCache = CACHE.get(group.name()); // do check. if (this.checkCacheDelayAndUpdate(serverCache, clientMd5, clientModifyTime)) { changedGroup.add(group); } } return changedGroup; }