c#滑窗緩存


前言

在大數據時代,軟件系統需要具備處理海量數據的能力,同時也更加依賴於系統強大的存儲能力與數據響應能力。各種大數據的工具如雨后春筍般孕育而生,這對於系統來說是極大的利好。但在后端采用分布式、雲存儲和虛擬化等技術大刀闊斧地解決大部分存儲問題后,仍然不足以滿足所有的業務需求。對於以用戶為終點的軟件系統來說,無論后台多么強大都難以避免有一部分數據流向終端,面向用戶。為了應對這最后一公里的通勤問題,我們得在終端緩存部分數據來提高系統的響應效率。另外一方面,受限於用戶終端的機器性能,緩存大量的數據反而會降低系統響應速度,甚至讓系統崩潰。為此,我們需要一個根據系統當前狀態動態調整最急需的數據的緩存器,滑窗緩存是一個很不錯的選擇。最終,我們找到了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 項目剛剛起步,歡迎提出改進意見。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM