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