c#滑窗快取

  • 2019 年 10 月 3 日
  • 筆記

前言

在大數據時代,軟體系統需要具備處理海量數據的能力,同時也更加依賴於系統強大的存儲能力與數據響應能力。各種大數據的工具如雨後春筍般孕育而生,這對於系統來說是極大的利好。但在後端採用分散式、雲存儲和虛擬化等技術大刀闊斧地解決大部分存儲問題後,仍然不足以滿足所有的業務需求。對於以用戶為終點的軟體系統來說,無論後台多麼強大都難以避免有一部分數據流向終端,面向用戶。為了應對這最後一公里的通勤問題,我們得在終端快取部分數據來提高系統的響應效率。另外一方面,受限於用戶終端的機器性能,快取大量的數據反而會降低系統響應速度,甚至讓系統崩潰。為此,我們需要一個根據系統當前狀態動態調整最急需的數據的快取器,滑窗快取是一個很不錯的選擇。最終,我們找到了SlidingWindowCache,一個基於 .NET standard 實現的滑窗快取。

SlidingWindowCache 簡介

SlidingWindowCache 基於鍵值對快取,可以快取以特定序列序列組織的數據,比如時間序列數據。其本身帶有預先快取的能力,當系統狀態滿足預設條件後將自動快取數據。每次自動快取的量可自行配置。當快取超出窗口後即被視為無用數據,會被自動釋放。同樣的,快取窗口大小可進行配置。

作為 key/value 快取,該快取的 value 可以是任意類型的數據。但為了滿足有序組織,目前的 key 只支援 int、long、float 和 double 四種類型。對於時間序列數據來說,可以將時間轉化為 long 作為 key 使用。後面將以 DataTime 轉為 Ticks 為例進行演示(事實上轉為時間戳更具有通用性),直接展示使用常式更加容易說明問題。

SlidingWindowCache 使用

SlidingWindowCache 配置

SlidingWindowCache 的絕大部分配置都在ISlidingWindowConfig<TKey>介面中定義,目前具有以下重要的配置:

  • TKey StartPoint { get; set; } —— 快取序列的起點
  • TKey EndPoint { get; set; } —— 快取序列的終點
  • TKey PerLoadSize { get; set; } —— 每次快取請求的大小。在自動快取中,將自動向數據源請求數據
  • TKey TotalLoadSize { get; set; } —— 總共載入的數據大小。在自動快取中,快取數據到達該閾值則停止自動快取
  • TKey TotalCacheSize { get; set; } —— 總共快取的數據大小。即滑動窗口的大小,超出該窗口的數據被自動釋放
  • int LoadParallelLimit { get; set; } —— 自動載入數據時並發量閾值
  • float LoadTriggerFrequency { get; set; } —— 載入觸發頻率。為 1 時,只要狀態一改變,立即觸發自動載入。
  • float RemoveTriggerFrequency { get; set; } —— 移除觸發頻率。為 1 時,只要狀態一改變,立即觸發自動移除。
  • float ForwardAndBackwardScale { get; set; } —— 前後比例(TKey 大端為前,習慣了以時間箭頭為前)。以快取大小來說,當前 TKey 作為分割點。

我們可以用形象的比喻來做進一步的解釋。StartPointEndPoint限定了窗體能滑動的邊界。TotalCacheSize限定了窗體的大小,在某種意義上來說,該窗體是殘破不堪的,因其並未隨時擁有所有的數據。它等待著修補匠進行破窗修補(數據源載入)。TotalLoadSize限定了每個狀態生命周期中修補破窗的總大小,也就是自動請求數據量的大小。PerLoadSize則為每次修補的大小,即每次向數據源請求的數據量。LoadParallelLimit可以理解為可以同時工作的修補匠的最多人數。LoadTriggerFrequency則可以理解為當狀態變更時,修補匠的出勤率。

SlidingWindowCache 快取

SlidingWindowCache 當前只提供少數重要的功能,全在ISlidingWindowCache<TKey, TData>介面中進行定義。

// 當前點,用來標記快取狀態  TKey CurrentPoint { get; set; }  // 當前快取的key的個數  int Count { get; }  // 從快取中獲取數據  Task<IEnumerable<TData>> GetCacheData(TKey start, TKey end, Func<TData, TKey> keyOfTData);  // 載入源數據的委託(必須進行賦值)  Func<TKey, TKey, CancellationToken, Task<IEnumerable<TData>>> DataSourceDelegate { get; set; }  // 自動載入任務狀態報告事件  event EventHandler<TaskStatus> OnDataAutoLoaderStatusChanged;

SlidingWindowCache 具體使用

下面以快取時間序列數據為例做一具體使用介紹

// 自定義數據模擬類  public class DataModel  {      private static readonly Lazy<DataModel> _lazy = new Lazy<DataModel>(() => new DataModel());      public static DataModel Instance => _lazy.Value;      public long Point { get; set; }        // 模擬大量數據,佔用記憶體      public long[] data = new long[1000];        // 模擬伺服器數據請求      public Task<IEnumerable<DataModel>> LoadDataFromSource(long s, long e,          CancellationToken cancellationToken)      {          return Task.Run(() =>          {              var rd = new Random();              // 模擬遠程訪問數據時可能的延遲              Task.Delay(rd.Next(50, 400), cancellationToken).Wait(cancellationToken);              var diff = (int)(e - s);              var count = diff > 100 ? 100 : diff;              var result = Enumerable.Range(0, count)                  .Select(t => new DataModel { Point = s + rd.Next(diff) })                  .OrderBy(t => t.Point)                  .ToList();              return (IEnumerable<DataModel>)result;          }, cancellationToken);      }  }    // 滑窗配置  var config = new SlidingWindowConfig<long>  {      PerLoadSize = new TimeSpan(0, 2, 0).Ticks,      StartPoint = new DateTime(2019, 1, 1).Ticks,      EndPoint = new DateTime(2019, 2, 1).Ticks,      TotalLoadSize = new TimeSpan(0, 30, 0).Ticks,      TotalCacheSize = new TimeSpan(7, 0, 0).Ticks  };    // 實例化快取器  var cache = new SlidingWindowCache<long, DataModel>(config)  {      // 提供獲取源數據的委託      DataSourceDelegate = DataModel.Instance.LoadDataFromSource,      CurrentPoint = config.StartPoint  };      // 獲取2019-1-1 0:1:39至2019-1-1 0:2:0之間的數據  // lamda表達式t => t.Point提供快取類型DataModel中的TKey的獲取方法,用於數據過濾  var data = await cache.GetCacheData(                  new DateTime(2019, 1, 1, 0, 1, 39).Ticks,                  new DateTime(2019, 1, 1, 0, 2, 0).Ticks,                  t => t.Point);

上述例子中,我們可能查看的數據總範圍為:2019-1-1 至 2019-2-1,總共為一個月的數據量。而終端機器允許快取的數據量最多只能有 7 個小時。為了減少伺服器壓力,每次請求兩分鐘的數據量,預先自動快取為半小時的數據量。在某一次數據獲取中(2019-1-1 0:1:39 至 2019-1-1 0:2:0),獲取 21 秒的數據,lamda 將提供自動篩選的憑據。隨著cache.CurrentPoint逐漸增加(這裡模擬時間增加),可以看到記憶體的大致變化趨勢:

隨著時間增加,記憶體使用量首先會持續增加,當達到設定閾值後便自動下降。此後,便在某一窗口之間重複震蕩。符合滑動窗口快取的預期。

後記

SlidingWindowCache 已經投入實際使用環境中,每次請求的量達到千級甚至萬級,總共快取的量達到百萬級別(後端使用 Hbase 作為最終的存儲方案,前端以 SlidingWindowCache 作為最前的快取方案)。

SlidingWindowCache 項目剛剛起步,歡迎提出改進意見。

Exit mobile version