九、soul源碼學習-http長輪訓數據同步機制詳解

上一節講了數據持久化后,發送事件后,Spring監聽到事件后,做了什么事,并看到現有四種數據同步機制。這節具體加一下http長輪訓

org.dromara.soul.admin.listener.http.HttpLongPollingDataChangedListener http長輪訓數據監聽器

先看下構造器:在構造器中,構造了一個1024長度的阻塞隊列,以及一個ScheduledThreadPoolExecutor,并初始化HttpSyncProperties,

/**
     * Blocked client.
     */
    private final BlockingQueue<LongPollingClient> clients;

    private final ScheduledExecutorService scheduler;

    private final HttpSyncProperties 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;
    }

HttpSyncProperties主要是http同步的配置

@Getter
@Setter
@ConfigurationProperties(prefix = "soul.sync.http")
public class HttpSyncProperties {

    /**
     * Whether enabled http sync strategy, default: true.
     */
    private boolean enabled = true;

    /**
     * Periodically refresh the config data interval from the database, default: 5 minutes.
     */
    private Duration refreshInterval = Duration.ofMinutes(5);

}

主要定義了http同步開關以及刷新周期。 類初始化之后,更新各種數據緩存,然后執行了一個定時任務,每次調用refreshLocalCache刷新本地緩存

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

@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);
}
private void refreshLocalCache() {
  this.updateAppAuthCache();
  this.updatePluginCache();
  this.updateRuleCache();
  this.updateSelectorCache();
  this.updateMetaDataCache();
}

這些方法要做的事情都很類似,就是從數據庫拿到對應ConfigGroup的所有配置,并更新本地緩存,比如看updateAppAuthCache,就是將當前數據庫的配置更新到本地緩存,至于具體為什么要更新到本地緩存,我們后面分曉。

//org.dromara.soul.admin.listener.AbstractDataChangedListener
protected static final ConcurrentMap<String, ConfigDataCache> CACHE = new ConcurrentHashMap<>();
/**
     * Update app auth cache.
     */
protected void updateAppAuthCache() {
  this.updateCache(ConfigGroupEnum.APP_AUTH, appAuthService.listAll());
}

protected <T> void updateCache(final ConfigGroupEnum group, final List<T> data) {
  String json = GsonUtils.getInstance().toJson(data);
  ConfigDataCache newVal = new ConfigDataCache(group.name(), json, Md5Utils.md5(json), System.currentTimeMillis());
  ConfigDataCache oldVal = CACHE.put(newVal.getGroup(), newVal);
  log.info("update config cache[{}], old: {}, updated: {}", group, oldVal, newVal);
}

之前我們看到,當數據事件變化監聽器分發者,監聽到事件后,會調用各個監聽器的對應方法:

//org.dromara.soul.admin.listener.DataChangedEventDispatcher
@Override
@SuppressWarnings("unchecked")
public void onApplicationEvent(final DataChangedEvent event) {
  for (DataChangedListener listener : listeners) {
    switch (event.getGroupKey()) {
      case APP_AUTH:
        listener.onAppAuthChanged((List<AppAuthData>) event.getSource(), event.getEventType());
        break;
      case PLUGIN:
        listener.onPluginChanged((List<PluginData>) event.getSource(), event.getEventType());
        break;
      case RULE:
        listener.onRuleChanged((List<RuleData>) event.getSource(), event.getEventType());
        break;
      case SELECTOR:
        listener.onSelectorChanged((List<SelectorData>) event.getSource(), event.getEventType());
        break;
      case META_DATA:
        listener.onMetaDataChanged((List<MetaData>) event.getSource(), event.getEventType());
        break;
      default:
        throw new IllegalStateException("Unexpected value: " + event.getGroupKey());
    }
  }
}

那么在長輪訓機制下,主要做了如下事情,還拿AppAuth看下

@Override
public void onAppAuthChanged(final List<AppAuthData> changed, final DataEventTypeEnum eventType) {
  if (CollectionUtils.isEmpty(changed)) {
    return;
  }
  this.updateAppAuthCache();
  this.afterAppAuthChanged(changed, eventType);
}

接收到變更數據后,會先更新下對應的內存緩存,然后再做數據變更。

//org.dromara.soul.admin.listener.http.HttpLongPollingDataChangedListener#afterAppAuthChanged
@Override
protected void afterAppAuthChanged(final List<AppAuthData> changed, final DataEventTypeEnum eventType) {
  scheduler.execute(new DataChangeTask(ConfigGroupEnum.APP_AUTH));
}

這里看到,通過線程池執行一個數據變化任務

        /**
     * When a group's data changes, the thread is created to notify the client asynchronously.
     */
    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() {
          //循環所有的LongPollingClient,并調用了sendResponse
            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);
            }
        }
    }

我們先來看下LongPollingClient是個什么東東,它主要有以下幾個屬性,一個異步的上下文,ip,超時時間和異步結果Future。LongPollingClient本身實現了一個Runnable接口

class LongPollingClient implements Runnable {           
                /**
         * The Async context.
         */
        private final AsyncContext asyncContext;

        /**
         * The Ip.
         */
        private final String ip;

        /**
         * The Timeout time.
         */
        private final long timeoutTime;

        /**
         * The Async timeout future.
         */
        private Future<?> asyncTimeoutFuture;

我們再來看下run方法:這方法較難看懂。

@Override
public void run() {
  //通過org.dromara.soul.admin.listener.http.HttpLongPollingDataChangedListener的ScheduledExecutorService scheduler延遲執行一個一次性的動作,延遲時間是timeoutTime毫秒,當延遲動作開始執行時,將當前的LongPollingClient對象從clients中移除
  this.asyncTimeoutFuture = scheduler.schedule(() -> {
    clients.remove(LongPollingClient.this);
    //1.1
    List<ConfigGroupEnum> changedGroups = compareChangedGroup((HttpServletRequest) asyncContext.getRequest());
    //1.2
    sendResponse(changedGroups);
  }, timeoutTime, TimeUnit.MILLISECONDS);
  //將當前對象加入到clients中
  clients.add(this);
}

這里LongPollingClient.this之前沒有見到過,主要是當我們在一個類的內部類中,如果需要訪問外部類的方法或者成員域的時候,如果直接使用 this.成員域(與 內部類.this.成員域 沒有分別) 調用的顯然是內部類的域 , 如果我們想要訪問外部類的域的時候,就要必須使用 外部類.this.成員域

package com.test;
public class TestA 
{    
    public void tn()
    {          
        System.out.println("外部類tn");         
    }  
    Thread thread = new Thread(){     
          public void tn(){System.out.println("inner tn");}        
          public void run(){           
                 System.out.println("內部類run");        
                 TestA.this.tn();//調用外部類的tn方法。          
                 this.tn();//調用內部類的tn方法           
             }    
     };          
     public static void main(String aaa[])
     {new TestA().thread.start();}
}

1.1 compareChangedGroup具體做了什么,先不要關注HttpServletRequest是從哪來的,這里也看出了我們本地Cache的作用是什么

private List<ConfigGroupEnum> compareChangedGroup(final HttpServletRequest request) {
  List<ConfigGroupEnum> changedGroup = new ArrayList<>(4);
  for (ConfigGroupEnum group : ConfigGroupEnum.values()) {
    // 針對每一個group獲取的對應的參數
    String[] params = StringUtils.split(request.getParameter(group.name()), ',');
    if (params == null || params.length != 2) {
      throw new SoulException("group param invalid:" + request.getParameter(group.name()));
    }
    //參數第一位時client端的Md5值, 第二位時client端的修改時間戳
    String clientMd5 = params[0];
    long clientModifyTime = NumberUtils.toLong(params[1]);
    //獲取本地緩存的配置
    ConfigDataCache serverCache = CACHE.get(group.name());
    // 檢查是否需要更新服務器的緩存配置
    //1.1.1
    if (this.checkCacheDelayAndUpdate(serverCache, clientMd5, clientModifyTime)) {
      changedGroup.add(group);
    }
  }
  return changedGroup;
}

1.1.1 checkCacheDelayAndUpdate

private boolean checkCacheDelayAndUpdate(final ConfigDataCache serverCache, final String clientMd5, final long clientModifyTime) {

  // 如果md5相等,說明配置相同,不需要更新
  if (StringUtils.equals(clientMd5, serverCache.getMd5())) {
    return false;
  }

  // 如果md5值不等,說明服務器的配置和客戶端的緩存不一致
  long lastModifyTime = serverCache.getLastModifyTime();
  //在比對下服務器配置是否比客戶端的更新
  if (lastModifyTime >= clientModifyTime) {
    // 如果更新,說明客戶端的配置是舊的,需要更新
    return true;
  }

  // 如果服務端的緩存配置,比客戶端的配置還要老,那么說明,服務端的緩存配置需要更新了
  // 這里soul考慮到并發問題,如果多個client都來soul拉取最新配置,而當前的soul-admin配置因為都會走到這里,那么如果我們不加鎖的話,會導致,同時走到后面的refreshLocalCache,而refreshLocalCache我們前面看到是需要查詢數據庫并更新到本地緩存的,那么會導致大量的sql查詢給數據庫帶來壓力,所以這里加了一個鎖,并設置了超時時間
  boolean locked = false;
  try {
    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();
    }
  }
  // 沒有獲取到鎖,默認當成需要更新處理
  return true;

}

上面的代碼,看出了soul設計的代碼的精妙之處

接著上面代碼,1.1之后,會調用sendResponse(changedGroups);

void sendResponse(final List<ConfigGroupEnum> changedGroups) {
  // 這里邏輯場景就是上面我們剛開始跟過來的DataChangeTask執行的run里面,對所有client的主動觸發的場景,這里是想取消掉client的run執行時候的延遲動作,防止重復運行,具體原因還需要在往后看
  if (null != asyncTimeoutFuture) {
    asyncTimeoutFuture.cancel(false);
  }
  //生成response,aysncContext完成
  generateResponse((HttpServletResponse) asyncContext.getResponse(), changedGroups);
  asyncContext.complete();
}

通過上面的源碼分析。我們現在主要有幾個疑惑點:

  1. AsyncContext到底是干嘛的?
  2. 為什么是直接生成的Response返回?
  3. Client是什么時候添加到org.dromara.soul.admin.listener.http.HttpLongPollingDataChangedListener#clients里面的

帶著這幾個問題,我們在看下一篇文章

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容