nacos 原理之爲什麼修改了配置文件後應用端會立刻生效-服務端篇1

單位用了 nacos 來保存配置,前一段時間研究了下 nacos 的原理,今天做個整理和總結。

爲什麼在 nacos 服務端改了配置文件之後,應用端會立刻生效?

原來我以爲服務端在修改了配置文件之後會把結果推送給應用端,後來看了代碼之後才發現不是這樣的。

簡單的說一下,在應用端有一個線程會不斷的查詢服務端,我感興趣的某些文件有沒有發生變化:

  1. 如果服務端的配置文件有了變化之後,會立刻告訴應用端,某些文件發生了改變。緊接着應用端會根據返回的改變了的文件信息再去 nacos 服務端查詢改變了的文件的文件內容。查到內容之後再做相關的變量的改變。
  2. 如果文件沒有改變,則 nacos 服務端會有一個異步任務,如果在超時時間內配置文件:
    1. 沒有改變,則到點兒後調度任務會響應給應用端,告訴應用端你關心的文件沒有發生變化
    2. 如果有改變的話,則會立即給應用端發送響應,告訴應用端,你關心的哪些文件發生了改變。

下面我們來上代碼來詳細的看一下這個過程。

應用端有一個線程在長輪詢 服務端的 /v1/cs/configs/listener 這個服務

/v1/cs/configs/listener 這個服務對應着 nacos 服務端的 com.alibaba.nacos.config.server.controller.ConfigController.listener() 這個方法

1. 讓我們來看看 listener 這個方法的實現:


	@PostMapping("/listener")
	@Secured(action = ActionTypes.READ, parser = ConfigResourceParser.class)
	public void listener(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
		String probeModify = request.getParameter("Listening-Configs");
		if (StringUtils.isBlank(probeModify)) {
			throw new IllegalArgumentException("invalid probeModify");
		}

		probeModify = URLDecoder.decode(probeModify, Constants.ENCODE);

		Map<String, String> clientMd5Map;
		try {
			clientMd5Map = MD5Util.getClientMd5Map(probeModify);
		}
		catch (Throwable e) {
			throw new IllegalArgumentException("invalid probeModify");
		}

		// do long-polling
		inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
	}
  1. probeModify 是由分隔符拼起來的一個字符串,這個變量代表着應用關心的文件和文件的內容的 md5 集合。

    每個文件是由 dataId + groupId + md5 + tenantId(namespaceId) 確定的。dataId + 單詞分隔符 + groupId + 單詞分隔符 + tenantId(namespaceId)+ 行分隔符 如果應用有多個配置文件的話,則每個配置文件之間由行分隔符分隔。這個分隔與客戶端的版本還有是否傳了 tenantId 有關,我這兒列出的只是其中的一種。

  2. 接下來我們再看看 clientMd5Map 這個變量

    clientMd5Map 這個 map 的 key 代表了應用關心的文件,value 則代表了應用端關心的文件的內容對應的 md5 值。

  3. 再看看 inner.doPollingConfig() 這個方法調用。

/**

輪詢接口
*/
public String doPollingConfig(HttpServletRequest request, HttpServletResponse response,
Map<String, String> clientMd5Map, int probeRequestSize)
throws IOException {

// 長輪詢
if (LongPollingService.isSupportLongPolling(request)) {
longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);
return HttpServletResponse.SC_OK + “”;
}

// else 兼容短輪詢邏輯
List changedGroups = MD5Util.compareMd5(request, response, clientMd5Map);

// 兼容短輪詢result
String oldResult = MD5Util.compareMd5OldResult(changedGroups);
String newResult = MD5Util.compareMd5ResultString(changedGroups);

String version = request.getHeader(Constants.CLIENT_VERSION_HEADER);
if (version == null) {
version =2.0.0;
}
int versionNum = Protocol.getVersionNumber(version);

/**

2.0.4版本以前, 返回值放入header中
*/
if (versionNum < START_LONG_POLLING_VERSION_NUM) {
response.addHeader(Constants.PROBE_MODIFY_RESPONSE, oldResult);
response.addHeader(Constants.PROBE_MODIFY_RESPONSE_NEW, newResult);
} else {
request.setAttribute(“content”, newResult);
}
Loggers.AUTH.info(new content:+ newResult);

// 禁用緩存
response.setHeader(“Pragma”, “no-cache”);
response.setDateHeader(“Expires”, 0);
response.setHeader(“Cache-Control”, “no-cache,no-store”);
response.setStatus(HttpServletResponse.SC_OK);
return HttpServletResponse.SC_OK + “”;
}

只看支持長輪詢的邏輯,短輪詢的邏輯更簡單。重點只看這一行代碼的調用

    longPollingService.addLongPollingClient(request, response, clientMd5Map, probeRequestSize);

把這個長輪詢客戶端請求添加到長輪詢服務中了。

2. 再接着看 com.alibaba.nacos.config.server.service.LongPollingService.addLongPollingClient() 這個方法

    public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
                                     int probeRequestSize) {

        String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
        String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
        String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
        String tag = req.getHeader("Vipserver-Tag");
        int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
        /**
         * 提前500ms返回響應,爲避免客戶端超時 @qiaoyi.dingqy 2013.10.22改動  add delay time for LoadBalance
         */
        long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
        if (isFixedPolling()) {
            timeout = Math.max(10000, getFixedPollingInterval());
            // do nothing but set fix polling timeout
        } else {
            long start = System.currentTimeMillis();
            List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
            if (changedGroups.size() > 0) {
                generateResponse(req, rsp, changedGroups);
                LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
                    System.currentTimeMillis() - start, "instant", RequestUtil.getRemoteIp(req), "polling",
                    clientMd5Map.size(), probeRequestSize, changedGroups.size());
                return;
            } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
                LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
                    RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                    changedGroups.size());
                return;
            }
        }
        String ip = RequestUtil.getRemoteIp(req);
        // 一定要由HTTP線程調用,否則離開後容器會立即發送響應
        final AsyncContext asyncContext = req.startAsync();
        // AsyncContext.setTimeout()的超時時間不準,所以只能自己控制
        asyncContext.setTimeout(0L);

        scheduler.execute(
            new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
    }

看了下代碼,總結了下固定輪詢和非固定輪詢的差異:

  1. 固定輪詢只有在定時調度任務到了之後纔會給出應用端響應,換句話說,如果服務器端修改了配置,固定輪詢有很大可能不能立即收到文件變更的響應。
  2. 非固定輪詢在詢問的時候首先會查看一下配置文件是否發生了改變,如果是的話,就立即給出響應。如果不是的話,則加入定時調度任務。如果定時調度還沒有開始執行,這時候服務端修改了配置文件,則會把定時調度任務取消,並且立即給出應用端響應,應用端幾乎是實時收到服務端給出的文件變更響應。

默認是非固定輪詢,首先查看應用端關心的配置文件是否發生了改變,即 changedGroups.size() 是否大於 0 。如果配置文件發生了改變的話,則立即給出響應,並告訴應用,哪些文件發生了改變。

如果配置文件沒有改變,並且 noHangUpFlag 爲 true 的時候,記錄完日誌之後就 return 了,由於沒有啓用異步調用,所以給應用響應的是空。

一般是在應用啓動的時候 noHangUpFlag 才爲真,應用首次加載配置文件,這個時候不能一直處於等待狀態。

再接着往下看,如果配置文件沒有改變並且應用端允許服務端掛起自己時,則會讓線程池調度任務立即執行 ClientLongPolling 任務。

        String ip = RequestUtil.getRemoteIp(req);
        // 一定要由HTTP線程調用,否則離開後容器會立即發送響應
        final AsyncContext asyncContext = req.startAsync();
        // AsyncContext.setTimeout()的超時時間不準,所以只能自己控制
        asyncContext.setTimeout(0L);

        scheduler.execute(
            new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));

ClientLongPolling 是 LongPollingService 的一個內部類:


    package com.alibaba.nacos.config.server.service;
    
    import com.alibaba.nacos.config.server.model.SampleResult;
    import com.alibaba.nacos.config.server.model.event.LocalDataChangeEvent;
    import com.alibaba.nacos.config.server.monitor.MetricsMonitor;
    import com.alibaba.nacos.config.server.utils.GroupKey;
    import com.alibaba.nacos.config.server.utils.LogUtil;
    import com.alibaba.nacos.config.server.utils.MD5Util;
    import com.alibaba.nacos.config.server.utils.RequestUtil;
    import com.alibaba.nacos.config.server.utils.event.EventDispatcher.AbstractEventListener;
    import com.alibaba.nacos.config.server.utils.event.EventDispatcher.Event;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.stereotype.Service;
    
    import javax.servlet.AsyncContext;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.*;
    import java.util.concurrent.*;
    
    import static com.alibaba.nacos.config.server.utils.LogUtil.memoryLog;
    import static com.alibaba.nacos.config.server.utils.LogUtil.pullLog;
    
    /**
     * 長輪詢服務。負責處理
     *
     * @author Nacos
     */
    @Service
    public class LongPollingService extends AbstractEventListener {
    
        private static final int FIXED_POLLING_INTERVAL_MS = 10000;
    
        private static final int SAMPLE_PERIOD = 100;
    
        private static final int SAMPLE_TIMES = 3;
    
        private static final String TRUE_STR = "true";
    
        private Map<String, Long> retainIps = new ConcurrentHashMap<String, Long>();
    
        private static boolean isFixedPolling() {
            return SwitchService.getSwitchBoolean(SwitchService.FIXED_POLLING, false);
        }
    
        private static int getFixedPollingInterval() {
            return SwitchService.getSwitchInteger(SwitchService.FIXED_POLLING_INTERVAL, FIXED_POLLING_INTERVAL_MS);
        }
    
        public boolean isClientLongPolling(String clientIp) {
            return getClientPollingRecord(clientIp) != null;
        }
    
        public Map<String, String> getClientSubConfigInfo(String clientIp) {
            ClientLongPolling record = getClientPollingRecord(clientIp);
    
            if (record == null) {
                return Collections.<String, String>emptyMap();
            }
    
            return record.clientMd5Map;
        }
    
        public SampleResult getSubscribleInfo(String dataId, String group, String tenant) {
            String groupKey = GroupKey.getKeyTenant(dataId, group, tenant);
            SampleResult sampleResult = new SampleResult();
            Map<String, String> lisentersGroupkeyStatus = new HashMap<String, String>(50);
    
            for (ClientLongPolling clientLongPolling : allSubs) {
                if (clientLongPolling.clientMd5Map.containsKey(groupKey)) {
                    lisentersGroupkeyStatus.put(clientLongPolling.ip, clientLongPolling.clientMd5Map.get(groupKey));
                }
            }
            sampleResult.setLisentersGroupkeyStatus(lisentersGroupkeyStatus);
            return sampleResult;
        }
    
        public SampleResult getSubscribleInfoByIp(String clientIp) {
            SampleResult sampleResult = new SampleResult();
            Map<String, String> lisentersGroupkeyStatus = new HashMap<String, String>(50);
    
            for (ClientLongPolling clientLongPolling : allSubs) {
                if (clientLongPolling.ip.equals(clientIp)) {
                    // 一個ip可能有多個監聽
                    if (!lisentersGroupkeyStatus.equals(clientLongPolling.clientMd5Map)) {
                        lisentersGroupkeyStatus.putAll(clientLongPolling.clientMd5Map);
                    }
                }
            }
            sampleResult.setLisentersGroupkeyStatus(lisentersGroupkeyStatus);
            return sampleResult;
        }
    
        /**
         * 聚合採樣結果中的採樣ip和監聽配置的信息;合併策略用後面的覆蓋前面的是沒有問題的
         *
         * @param sampleResults sample Results
         * @return Results
         */
        public SampleResult mergeSampleResult(List<SampleResult> sampleResults) {
            SampleResult mergeResult = new SampleResult();
            Map<String, String> lisentersGroupkeyStatus = new HashMap<String, String>(50);
            for (SampleResult sampleResult : sampleResults) {
                Map<String, String> lisentersGroupkeyStatusTmp = sampleResult.getLisentersGroupkeyStatus();
                for (Map.Entry<String, String> entry : lisentersGroupkeyStatusTmp.entrySet()) {
                    lisentersGroupkeyStatus.put(entry.getKey(), entry.getValue());
                }
            }
            mergeResult.setLisentersGroupkeyStatus(lisentersGroupkeyStatus);
            return mergeResult;
        }
    
        public Map<String, Set<String>> collectApplicationSubscribeConfigInfos() {
            if (allSubs == null || allSubs.isEmpty()) {
                return null;
            }
            HashMap<String, Set<String>> app2Groupkeys = new HashMap<String, Set<String>>(50);
            for (ClientLongPolling clientLongPolling : allSubs) {
                if (StringUtils.isEmpty(clientLongPolling.appName) || "unknown".equalsIgnoreCase(
                    clientLongPolling.appName)) {
                    continue;
                }
                Set<String> appSubscribeConfigs = app2Groupkeys.get(clientLongPolling.appName);
                Set<String> clientSubscribeConfigs = clientLongPolling.clientMd5Map.keySet();
                if (appSubscribeConfigs == null) {
                    appSubscribeConfigs = new HashSet<String>(clientSubscribeConfigs.size());
                }
                appSubscribeConfigs.addAll(clientSubscribeConfigs);
                app2Groupkeys.put(clientLongPolling.appName, appSubscribeConfigs);
            }
    
            return app2Groupkeys;
        }
    
        public SampleResult getCollectSubscribleInfo(String dataId, String group, String tenant) {
            List<SampleResult> sampleResultLst = new ArrayList<SampleResult>(50);
            for (int i = 0; i < SAMPLE_TIMES; i++) {
                SampleResult sampleTmp = getSubscribleInfo(dataId, group, tenant);
                if (sampleTmp != null) {
                    sampleResultLst.add(sampleTmp);
                }
                if (i < SAMPLE_TIMES - 1) {
                    try {
                        Thread.sleep(SAMPLE_PERIOD);
                    } catch (InterruptedException e) {
                        LogUtil.clientLog.error("sleep wrong", e);
                    }
                }
            }
    
            SampleResult sampleResult = mergeSampleResult(sampleResultLst);
            return sampleResult;
        }
    
        public SampleResult getCollectSubscribleInfoByIp(String ip) {
            SampleResult sampleResult = new SampleResult();
            sampleResult.setLisentersGroupkeyStatus(new HashMap<String, String>(50));
            for (int i = 0; i < SAMPLE_TIMES; i++) {
                SampleResult sampleTmp = getSubscribleInfoByIp(ip);
                if (sampleTmp != null) {
                    if (sampleTmp.getLisentersGroupkeyStatus() != null
                        && !sampleResult.getLisentersGroupkeyStatus().equals(sampleTmp.getLisentersGroupkeyStatus())) {
                        sampleResult.getLisentersGroupkeyStatus().putAll(sampleTmp.getLisentersGroupkeyStatus());
                    }
                }
                if (i < SAMPLE_TIMES - 1) {
                    try {
                        Thread.sleep(SAMPLE_PERIOD);
                    } catch (InterruptedException e) {
                        LogUtil.clientLog.error("sleep wrong", e);
                    }
                }
            }
            return sampleResult;
        }
    
        private ClientLongPolling getClientPollingRecord(String clientIp) {
            if (allSubs == null) {
                return null;
            }
    
            for (ClientLongPolling clientLongPolling : allSubs) {
                HttpServletRequest request = (HttpServletRequest) clientLongPolling.asyncContext.getRequest();
    
                if (clientIp.equals(RequestUtil.getRemoteIp(request))) {
                    return clientLongPolling;
                }
            }
    
            return null;
        }
    
        public void addLongPollingClient(HttpServletRequest req, HttpServletResponse rsp, Map<String, String> clientMd5Map,
                                         int probeRequestSize) {
    
            String str = req.getHeader(LongPollingService.LONG_POLLING_HEADER);
            String noHangUpFlag = req.getHeader(LongPollingService.LONG_POLLING_NO_HANG_UP_HEADER);
            String appName = req.getHeader(RequestUtil.CLIENT_APPNAME_HEADER);
            String tag = req.getHeader("Vipserver-Tag");
            int delayTime = SwitchService.getSwitchInteger(SwitchService.FIXED_DELAY_TIME, 500);
            /**
             * 提前500ms返回響應,爲避免客戶端超時 @qiaoyi.dingqy 2013.10.22改動  add delay time for LoadBalance
             */
            long timeout = Math.max(10000, Long.parseLong(str) - delayTime);
            if (isFixedPolling()) {
                timeout = Math.max(10000, getFixedPollingInterval());
                // do nothing but set fix polling timeout
            } else {
                long start = System.currentTimeMillis();
                List<String> changedGroups = MD5Util.compareMd5(req, rsp, clientMd5Map);
                if (changedGroups.size() > 0) {
                    generateResponse(req, rsp, changedGroups);
                    LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
                        System.currentTimeMillis() - start, "instant", RequestUtil.getRemoteIp(req), "polling",
                        clientMd5Map.size(), probeRequestSize, changedGroups.size());
                    return;
                } else if (noHangUpFlag != null && noHangUpFlag.equalsIgnoreCase(TRUE_STR)) {
                    LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}", System.currentTimeMillis() - start, "nohangup",
                        RequestUtil.getRemoteIp(req), "polling", clientMd5Map.size(), probeRequestSize,
                        changedGroups.size());
                    return;
                }
            }
            String ip = RequestUtil.getRemoteIp(req);
            // 一定要由HTTP線程調用,否則離開後容器會立即發送響應
            final AsyncContext asyncContext = req.startAsync();
            // AsyncContext.setTimeout()的超時時間不準,所以只能自己控制
            asyncContext.setTimeout(0L);
    
            scheduler.execute(
                new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));
        }
    
        @Override
        public List<Class<? extends Event>> interest() {
            List<Class<? extends Event>> eventTypes = new ArrayList<Class<? extends Event>>();
            eventTypes.add(LocalDataChangeEvent.class);
            return eventTypes;
        }
    
        @Override
        public void onEvent(Event event) {
            if (isFixedPolling()) {
                // ignore
            } else {
                if (event instanceof LocalDataChangeEvent) {
                    LocalDataChangeEvent evt = (LocalDataChangeEvent)event;
                    scheduler.execute(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
                }
            }
        }
    
        static public boolean isSupportLongPolling(HttpServletRequest req) {
            return null != req.getHeader(LONG_POLLING_HEADER);
        }
    
        @SuppressWarnings("PMD.ThreadPoolCreationRule")
        public LongPollingService() {
            allSubs = new ConcurrentLinkedQueue<ClientLongPolling>();
    
            scheduler = Executors.newScheduledThreadPool(1, new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    Thread t = new Thread(r);
                    t.setDaemon(true);
                    t.setName("com.alibaba.nacos.LongPolling");
                    return t;
                }
            });
            scheduler.scheduleWithFixedDelay(new StatTask(), 0L, 10L, TimeUnit.SECONDS);
        }
    
        // =================
    
        static public final String LONG_POLLING_HEADER = "Long-Pulling-Timeout";
        static public final String LONG_POLLING_NO_HANG_UP_HEADER = "Long-Pulling-Timeout-No-Hangup";
    
        final ScheduledExecutorService scheduler;
    
        /**
         * 長輪詢訂閱關係
         */
        final Queue<ClientLongPolling> allSubs;
    
        // =================
    
        class DataChangeTask implements Runnable {
            @Override
            public void run() {
                try {
                    ConfigCacheService.getContentBetaMd5(groupKey);
                    for (Iterator<ClientLongPolling> iter = allSubs.iterator(); iter.hasNext(); ) {
                        ClientLongPolling clientSub = iter.next();
                        if (clientSub.clientMd5Map.containsKey(groupKey)) {
                            // 如果beta發佈且不在beta列表直接跳過
                            if (isBeta && !betaIps.contains(clientSub.ip)) {
                                continue;
                            }
    
                            // 如果tag發佈且不在tag列表直接跳過
                            if (StringUtils.isNotBlank(tag) && !tag.equals(clientSub.tag)) {
                                continue;
                            }
    
                            getRetainIps().put(clientSub.ip, System.currentTimeMillis());
                            iter.remove(); // 刪除訂閱關係
                            LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}|{}",
                                (System.currentTimeMillis() - changeTime),
                                "in-advance",
                                RequestUtil.getRemoteIp((HttpServletRequest)clientSub.asyncContext.getRequest()),
                                "polling",
                                clientSub.clientMd5Map.size(), clientSub.probeRequestSize, groupKey);
                            clientSub.sendResponse(Arrays.asList(groupKey));
                        }
                    }
                } catch (Throwable t) {
                    LogUtil.defaultLog.error("data change error:" + t.getMessage(), t.getCause());
                }
            }
    
            DataChangeTask(String groupKey) {
                this(groupKey, false, null);
            }
    
            DataChangeTask(String groupKey, boolean isBeta, List<String> betaIps) {
                this(groupKey, isBeta, betaIps, null);
            }
    
            DataChangeTask(String groupKey, boolean isBeta, List<String> betaIps, String tag) {
                this.groupKey = groupKey;
                this.isBeta = isBeta;
                this.betaIps = betaIps;
                this.tag = tag;
            }
    
            final String groupKey;
            final long changeTime = System.currentTimeMillis();
            final boolean isBeta;
            final List<String> betaIps;
            final String tag;
        }
    
        // =================
    
        class StatTask implements Runnable {
            @Override
            public void run() {
                memoryLog.info("[long-pulling] client count " + allSubs.size());
                MetricsMonitor.getLongPollingMonitor().set(allSubs.size());
            }
        }
    
        // =================
    
        class ClientLongPolling implements Runnable {
    
            @Override
            public void run() {
                asyncTimeoutFuture = scheduler.schedule(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
                            /**
                             * 刪除訂閱關係
                             */
                            allSubs.remove(ClientLongPolling.this);
    
                            if (isFixedPolling()) {
                                LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}",
                                    (System.currentTimeMillis() - createTime),
                                    "fix", RequestUtil.getRemoteIp((HttpServletRequest)asyncContext.getRequest()),
                                    "polling",
                                    clientMd5Map.size(), probeRequestSize);
                                List<String> changedGroups = MD5Util.compareMd5(
                                    (HttpServletRequest)asyncContext.getRequest(),
                                    (HttpServletResponse)asyncContext.getResponse(), clientMd5Map);
                                if (changedGroups.size() > 0) {
                                    sendResponse(changedGroups);
                                } else {
                                    sendResponse(null);
                                }
                            } else {
                                LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}",
                                    (System.currentTimeMillis() - createTime),
                                    "timeout", RequestUtil.getRemoteIp((HttpServletRequest)asyncContext.getRequest()),
                                    "polling",
                                    clientMd5Map.size(), probeRequestSize);
                                sendResponse(null);
                            }
                        } catch (Throwable t) {
                            LogUtil.defaultLog.error("long polling error:" + t.getMessage(), t.getCause());
                        }
    
                    }
    
                }, timeoutTime, TimeUnit.MILLISECONDS);
    
                allSubs.add(this);
            }
    
            void sendResponse(List<String> changedGroups) {
                /**
                 *  取消超時任務
                 */
                if (null != asyncTimeoutFuture) {
                    asyncTimeoutFuture.cancel(false);
                }
                generateResponse(changedGroups);
            }
    
            void generateResponse(List<String> changedGroups) {
                if (null == changedGroups) {
                    /**
                     * 告訴容器發送HTTP響應
                     */
                    asyncContext.complete();
                    return;
                }
    
                HttpServletResponse response = (HttpServletResponse)asyncContext.getResponse();
    
                try {
                    String respString = MD5Util.compareMd5ResultString(changedGroups);
    
                    // 禁用緩存
                    response.setHeader("Pragma", "no-cache");
                    response.setDateHeader("Expires", 0);
                    response.setHeader("Cache-Control", "no-cache,no-store");
                    response.setStatus(HttpServletResponse.SC_OK);
                    response.getWriter().println(respString);
                    asyncContext.complete();
                } catch (Exception se) {
                    pullLog.error(se.toString(), se);
                    asyncContext.complete();
                }
            }
    
            ClientLongPolling(AsyncContext ac, Map<String, String> clientMd5Map, String ip, int probeRequestSize,
                              long timeoutTime, String appName, String tag) {
                this.asyncContext = ac;
                this.clientMd5Map = clientMd5Map;
                this.probeRequestSize = probeRequestSize;
                this.createTime = System.currentTimeMillis();
                this.ip = ip;
                this.timeoutTime = timeoutTime;
                this.appName = appName;
                this.tag = tag;
            }
    
            // =================
    
            final AsyncContext asyncContext;
            final Map<String, String> clientMd5Map;
            final long createTime;
            final String ip;
            final String appName;
            final String tag;
            final int probeRequestSize;
            final long timeoutTime;
    
            Future<?> asyncTimeoutFuture;
        }
    
        void generateResponse(HttpServletRequest request, HttpServletResponse response, List<String> changedGroups) {
            if (null == changedGroups) {
                return;
            }
    
            try {
                String respString = MD5Util.compareMd5ResultString(changedGroups);
                // 禁用緩存
                response.setHeader("Pragma", "no-cache");
                response.setDateHeader("Expires", 0);
                response.setHeader("Cache-Control", "no-cache,no-store");
                response.setStatus(HttpServletResponse.SC_OK);
                response.getWriter().println(respString);
            } catch (Exception se) {
                pullLog.error(se.toString(), se);
            }
        }
    
        public Map<String, Long> getRetainIps() {
            return retainIps;
        }
    
        public void setRetainIps(Map<String, Long> retainIps) {
            this.retainIps = retainIps;
        }
    
    }

代碼挺長的,在這裏我們先看 ClientLongPolling 的 run() 方法。

    @Override
    public void run() {
        asyncTimeoutFuture = scheduler.schedule(new Runnable() {
            @Override
            public void run() {
                try {
                    getRetainIps().put(ClientLongPolling.this.ip, System.currentTimeMillis());
                    /**
                     * 刪除訂閱關係
                     */
                    allSubs.remove(ClientLongPolling.this);

                    if (isFixedPolling()) {
                        LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}",
                            (System.currentTimeMillis() - createTime),
                            "fix", RequestUtil.getRemoteIp((HttpServletRequest)asyncContext.getRequest()),
                            "polling",
                            clientMd5Map.size(), probeRequestSize);
                        List<String> changedGroups = MD5Util.compareMd5(
                            (HttpServletRequest)asyncContext.getRequest(),
                            (HttpServletResponse)asyncContext.getResponse(), clientMd5Map);
                        if (changedGroups.size() > 0) {
                            sendResponse(changedGroups);
                        } else {
                            sendResponse(null);
                        }
                    } else {
                        LogUtil.clientLog.info("{}|{}|{}|{}|{}|{}",
                            (System.currentTimeMillis() - createTime),
                            "timeout", RequestUtil.getRemoteIp((HttpServletRequest)asyncContext.getRequest()),
                            "polling",
                            clientMd5Map.size(), probeRequestSize);
                        sendResponse(null);
                    }
                } catch (Throwable t) {
                    LogUtil.defaultLog.error("long polling error:" + t.getMessage(), t.getCause());
                }

            }

        }, timeoutTime, TimeUnit.MILLISECONDS);

        allSubs.add(this);
    }

在 run() 方法裏做了兩件事:

  1. 第一件事就是用線程池調度任務又調度了一個任務。
  2. 第二件事就是把 ClientLongPolling 加入到 allSubs 客戶端訂閱集合中。

先看第一件事,過 timeoutTime 時間後會執行這個任務,再看這個任務幹了些啥,挑重點的看一下:

   allSubs.remove(ClientLongPolling.this);

刪除訂閱關係,爲什麼要刪除呢?因爲客戶端會長輪詢,並不是服務端推送給客戶端,如果不刪除的話,客戶端每輪詢一次就往進去添加一次, allSubs 會越來越大,最終結果就是把內存撐爆了。

如果是固定輪詢的話,則檢查應用端關心的文件是否發生了改變,如果改變了的話則把改變了的文件 key 信息響應給應用,如果沒改變的話,則響應數據爲空。

因爲服務端在修改配置文件的時候不會通知固定輪詢,所以固定輪詢只能在定時調度任務執行的時候去檢查是否文件發生了變更,檢查的間隔時間是固定的,因此叫固定輪詢。

如果是非固定輪詢的話,則給應用一個空的響應。

爲什麼非固定輪詢給一個空的響應呢?因爲非固定輪詢在詢問之初就會檢查一下配置文件是否發生了改變,如果發生了改變了的話就會立即給出響應。還有就是應用處於等待狀態,在這個等待過程中如果服務端修改了配置文件則會取消定時調度任務,然後立即給出響應。所以在定時調度任務中給出空的響應,文件發生了變更會在別的地方給出響應的。

再接着看看第二件事:

 allSubs.add(this);

爲什麼要把 this(ClientLongPolling) 加入到 allSubs 中呢?這兒埋下了伏筆,爲以後服務端修改了配置文件後取消定時任務/立即給出應用端響應埋下了伏筆。

花了一個簡單的流程圖,省略了一些內容,保留了主幹流程來幫助大家理解。

在這裏插入圖片描述

涉及到的內容不少,還有一大片代碼,剩下的部分會在下一篇中講述。服務端在修改配置文件的時候做了哪些事。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章