Soul网关源码阅读(九)- http长轮询同步数据(2)

学习目标:

http长轮询数据同步原理

学习内容:

1.如何感知数据的变化
2.数据是否实时同步

学习时间:

2020年1月26号

学习产出:

1.admin数据同步
ConfigController 这个controller里面有一个HttpLongPollingDataChangedListener属性。
HttpLongPollingDataChangedListener继承自AbstractDataChangedListener。而AbstractDataChangedListener实现了DataChangedListener接口和InitializingBean接口;
DataChangedListener监听所有类型数据的变化

public interface DataChangedListener {

    /**
     * invoke this method when AppAuth was received.
     *
     * @param changed   the changed
     * @param eventType the event type
     */
    default void onAppAuthChanged(List<AppAuthData> changed, DataEventTypeEnum eventType) {
    }

    /**
     * invoke this method when Plugin was received.
     *
     * @param changed   the changed
     * @param eventType the event type
     */
    default void onPluginChanged(List<PluginData> changed, DataEventTypeEnum eventType) {
    }

    /**
     * invoke this method when Selector was received.
     *
     * @param changed   the changed
     * @param eventType the event type
     */
    default void onSelectorChanged(List<SelectorData> changed, DataEventTypeEnum eventType) {
    }

    /**
     * On meta data changed.
     *
     * @param changed   the changed
     * @param eventType the event type
     */
    default void onMetaDataChanged(List<MetaData> changed, DataEventTypeEnum eventType) {

    }

    /**
     * invoke this method when Rule was received.
     *
     * @param changed   the changed
     * @param eventType the event type
     */
    default void onRuleChanged(List<RuleData> changed, DataEventTypeEnum eventType) {
    }

}

2.DataChangedListener 监听到数据变化首先更新admin本地缓存,然后触发一个任务:DataChangeTask
例如:

 protected void afterSelectorChanged(final List<SelectorData> changed, final DataEventTypeEnum eventType) {
        scheduler.execute(new DataChangeTask(ConfigGroupEnum.SELECTOR));
    }
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);
            }
        }
    }

clients存储内容后面会提及。

HttpLongPollingDataChangedListener的构造函数里面,初始化了一个大小为1024的ArrayBlockingQueue(这个queue是用来存LongPollingClient的,后面会提到),还初始化了一个定时线程池,以及一个http的同步策略对象httpSyncProperties。

/**
     * Instantiates a new Http long polling data changed listener.
     * @param httpSyncProperties the HttpSyncProperties
     */
    public HttpLongPollingDataChangedListener(final HttpSyncProperties httpSyncProperties) {
        this.clients = new ArrayBlockingQueue<>(1024);
        this.scheduler = new ScheduledThreadPoolExecutor(1,
                SoulThreadFactory.create("long-polling", true));
        this.httpSyncProperties = httpSyncProperties;
    }

上面提到过,AbstractDataChangedListener实现了InitializingBean接口,而InitializingBean有一个afterPropertiesSet方法。看下AbstractDataChangedListener里面对于afterPropertiesSet的具体实现:

@Override
    public final void afterPropertiesSet() {
        updateAppAuthCache();
        updatePluginCache();
        updateRuleCache();
        updateSelectorCache();
        updateMetaDataCache();
        afterInitialize();
    }

afterInitialize 会开启一个任务5分钟刷新一次本地缓存

@Override
    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);
    }

在http长轮询数据同步机制里面,soul-admin管理后台的/configs/fetch接口和/configs/listener接口会被soul网关调用。
soul-admin的/configs/fetch接口。它的主要逻辑,就是从soul-admin的内存中取出缓存的相应group的配置数据,并返回

soul-admin的/configs/listener接口,它调用了HttpLongPollingDataChangedListener的doLongPolling方法。
doLongPolling主要逻辑:

  1. 如果group发生更新,返回response;
  2. 如果没有,会利用Servlet的异步响应
    使用线程池立即执行LongPollingClient这个Runnable的run方法
    在这个run方法里面,会将this加入到上文提到的ArrayBlockingQueue里面
    会使用定时线程池,在60s之后,执行里面的具体逻辑。具体逻辑为:
    从ArrayBlockingQueue里面移除this
    将request里面的md5和lastModifyTime,和 当前内存中的比较,判断哪些group的数据有更新。将group的名字作为resposne返回。这里再次比较一下,是为了防止在这60s以内数据有变动。
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;
        }

        // listen for configuration changed.
        final AsyncContext asyncContext = request.startAsync();

        // AsyncContext.settimeout() does not timeout properly, so you have to control it yourself
        asyncContext.setTimeout(0L);

        // block client's thread.
        scheduler.execute(new LongPollingClient(asyncContext, clientIp, HttpConstants.SERVER_MAX_HOLD_TIMEOUT));
    }
    private List<ConfigGroupEnum> compareChangedGroup(final HttpServletRequest request) {
        List<ConfigGroupEnum> changedGroup = new ArrayList<>(4);
        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;
    }
	LongPollingClient(final AsyncContext ac, final String ip, final long timeoutTime) {
            this.asyncContext = ac;
            this.ip = ip;
            this.timeoutTime = timeoutTime;
        }

        @Override
        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);
        }

如果只是这样的话,在soul-admin发生配置变化的时候,还不够实时。因为网关可能不止一台机器,可能是个集群,集群里的每台机器都会和admin建立http长轮询,后面的http请求会被前面的block住,所以如果每次都等前面一个请求60s的话,当数据有变动的时候后面的请求很晚才能知道。所以当管理员进行一些改动的时候,希望所有与admin建立连接的每台网关服务器都能够感知到数据变动。
这个就是前面提到的clients的作用,数据发生变化时,DataChangeTask 会实时相应所有的clients请求;

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);
            }
        }
    }

猜你喜欢

转载自blog.csdn.net/koutann2015/article/details/113172312
今日推荐