前言
在大數(shù)據(jù)時(shí)代,軟件系統(tǒng)需要具備處理海量數(shù)據(jù)的能力,同時(shí)也更加依賴于系統(tǒng)強(qiáng)大的存儲(chǔ)能力與數(shù)據(jù)響應(yīng)能力。各種大數(shù)據(jù)的工具如雨后春筍般孕育而生,這對(duì)于系統(tǒng)來說是極大的利好。但在后端采用分布式、云存儲(chǔ)和虛擬化等技術(shù)大刀闊斧地解決大部分存儲(chǔ)問題后,仍然不足以滿足所有的業(yè)務(wù)需求。對(duì)于以用戶為終點(diǎn)的軟件系統(tǒng)來說,無論后臺(tái)多么強(qiáng)大都難以避免有一部分?jǐn)?shù)據(jù)流向終端,面向用戶。為了應(yīng)對(duì)這最后一公里的通勤問題,我們得在終端緩存部分?jǐn)?shù)據(jù)來提高系統(tǒng)的響應(yīng)效率。另外一方面,受限于用戶終端的機(jī)器性能,緩存大量的數(shù)據(jù)反而會(huì)降低系統(tǒng)響應(yīng)速度,甚至讓系統(tǒng)崩潰。為此,我們需要一個(gè)根據(jù)系統(tǒng)當(dāng)前狀態(tài)動(dòng)態(tài)調(diào)整最急需的數(shù)據(jù)的緩存器,滑窗緩存是一個(gè)很不錯(cuò)的選擇。最終,我們找到了
SlidingWindowCache <https://github.com/hsxian/SlidingWindowCache>,一個(gè)基于 .NET
standard 實(shí)現(xiàn)的滑窗緩存。
SlidingWindowCache 簡(jiǎn)介
SlidingWindowCache
基于鍵值對(duì)緩存,可以緩存以特定序列序列組織的數(shù)據(jù),比如時(shí)間序列數(shù)據(jù)。其本身帶有預(yù)先緩存的能力,當(dāng)系統(tǒng)狀態(tài)滿足預(yù)設(shè)條件后將自動(dòng)緩存數(shù)據(jù)。每次自動(dòng)緩存的量可自行配置。當(dāng)緩存超出窗口后即被視為無用數(shù)據(jù),會(huì)被自動(dòng)釋放。同樣的,緩存窗口大小可進(jìn)行配置。
作為 key/value 緩存,該緩存的 value 可以是任意類型的數(shù)據(jù)。但為了滿足有序組織,目前的 key 只支持 int、long、float 和
double 四種類型。對(duì)于時(shí)間序列數(shù)據(jù)來說,可以將時(shí)間轉(zhuǎn)化為 long 作為 key 使用。后面將以 DataTime 轉(zhuǎn)為 Ticks
為例進(jìn)行演示(事實(shí)上轉(zhuǎn)為時(shí)間戳更具有通用性),直接展示使用例程更加容易說明問題。
SlidingWindowCache 使用
SlidingWindowCache 配置
SlidingWindowCache 的絕大部分配置都在ISlidingWindowConfig<TKey>接口中定義,目前具有以下重要的配置:
* TKey StartPoint { get; set; } —— 緩存序列的起點(diǎn)
* TKey EndPoint { get; set; } —— 緩存序列的終點(diǎn)
* TKey PerLoadSize { get; set; } —— 每次緩存請(qǐng)求的大小。在自動(dòng)緩存中,將自動(dòng)向數(shù)據(jù)源請(qǐng)求數(shù)據(jù)
* TKey TotalLoadSize { get; set; } —— 總共加載的數(shù)據(jù)大小。在自動(dòng)緩存中,緩存數(shù)據(jù)到達(dá)該閾值則停止自動(dòng)緩存
* TKey TotalCacheSize { get; set; } —— 總共緩存的數(shù)據(jù)大小。即滑動(dòng)窗口的大小,超出該窗口的數(shù)據(jù)被自動(dòng)釋放
* int LoadParallelLimit { get; set; } —— 自動(dòng)加載數(shù)據(jù)時(shí)并發(fā)量閾值
* float LoadTriggerFrequency { get; set; } —— 加載觸發(fā)頻率。為 1 時(shí),只要狀態(tài)一改變,立即觸發(fā)自動(dòng)加載。
* float RemoveTriggerFrequency { get; set; } —— 移除觸發(fā)頻率。為 1 時(shí),只要狀態(tài)一改變,立即觸發(fā)自動(dòng)移除。
* float ForwardAndBackwardScale { get; set; } —— 前后比例(TKey
大端為前,習(xí)慣了以時(shí)間箭頭為前)。以緩存大小來說,當(dāng)前 TKey 作為分割點(diǎn)。
我們可以用形象的比喻來做進(jìn)一步的解釋。StartPoint和EndPoint限定了窗體能滑動(dòng)的邊界。TotalCacheSize
限定了窗體的大小,在某種意義上來說,該窗體是殘破不堪的,因其并未隨時(shí)擁有所有的數(shù)據(jù)。它等待著修補(bǔ)匠進(jìn)行破窗修補(bǔ)(數(shù)據(jù)源加載)。TotalLoadSize
限定了每個(gè)狀態(tài)生命周期中修補(bǔ)破窗的總大小,也就是自動(dòng)請(qǐng)求數(shù)據(jù)量的大小。PerLoadSize則為每次修補(bǔ)的大小,即每次向數(shù)據(jù)源請(qǐng)求的數(shù)據(jù)量。
LoadParallelLimit可以理解為可以同時(shí)工作的修補(bǔ)匠的最多人數(shù)。LoadTriggerFrequency則可以理解為當(dāng)狀態(tài)變更時(shí),修補(bǔ)匠的出勤率。
SlidingWindowCache 緩存
SlidingWindowCache 當(dāng)前只提供少數(shù)重要的功能,全在ISlidingWindowCache<TKey, TData>接口中進(jìn)行定義。
// 當(dāng)前點(diǎn),用來標(biāo)記緩存狀態(tài) TKey CurrentPoint { get; set; } // 當(dāng)前緩存的key的個(gè)數(shù) int Count {
get; } // 從緩存中獲取數(shù)據(jù) Task<IEnumerable<TData>> GetCacheData(TKey start, TKey end,
Func<TData, TKey> keyOfTData); // 加載源數(shù)據(jù)的委托(必須進(jìn)行賦值) Func<TKey, TKey,
CancellationToken, Task<IEnumerable<TData>>> DataSourceDelegate { get; set; }
// 自動(dòng)加載任務(wù)狀態(tài)報(bào)告事件 event EventHandler<TaskStatus> OnDataAutoLoaderStatusChanged;
SlidingWindowCache 具體使用
下面以緩存時(shí)間序列數(shù)據(jù)為例做一具體使用介紹
// 自定義數(shù)據(jù)模擬類 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; } // 模擬大量數(shù)據(jù),占用內(nèi)存 public
long[] data = new long[1000]; // 模擬服務(wù)器數(shù)據(jù)請(qǐng)求 public Task<IEnumerable<DataModel>>
LoadDataFromSource(long s, long e, CancellationToken cancellationToken) {
return Task.Run(() => { var rd = new Random(); // 模擬遠(yuǎn)程訪問數(shù)據(jù)時(shí)可能的延遲
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 }; // 實(shí)例化緩存器 var cache = new SlidingWindowCache<long,
DataModel>(config) { // 提供獲取源數(shù)據(jù)的委托 DataSourceDelegate =
DataModel.Instance.LoadDataFromSource, CurrentPoint = config.StartPoint }; //
獲取2019-1-1 0:1:39至2019-1-1 0:2:0之間的數(shù)據(jù) // lamda表達(dá)式t =>
t.Point提供緩存類型DataModel中的TKey的獲取方法,用于數(shù)據(jù)過濾 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);
上述例子中,我們可能查看的數(shù)據(jù)總范圍為:2019-1-1 至 2019-2-1,總共為一個(gè)月的數(shù)據(jù)量。而終端機(jī)器允許緩存的數(shù)據(jù)量最多只能有 7
個(gè)小時(shí)。為了減少服務(wù)器壓力,每次請(qǐng)求兩分鐘的數(shù)據(jù)量,預(yù)先自動(dòng)緩存為半小時(shí)的數(shù)據(jù)量。在某一次數(shù)據(jù)獲取中(2019-1-1 0:1:39 至 2019-1-1
0:2:0),獲取 21 秒的數(shù)據(jù),lamda 將提供自動(dòng)篩選的憑據(jù)。隨著cache.CurrentPoint
逐漸增加(這里模擬時(shí)間增加),可以看到內(nèi)存的大致變化趨勢(shì):
隨著時(shí)間增加,內(nèi)存使用量首先會(huì)持續(xù)增加,當(dāng)達(dá)到設(shè)定閾值后便自動(dòng)下降。此后,便在某一窗口之間重復(fù)震蕩。符合滑動(dòng)窗口緩存的預(yù)期。
后記
SlidingWindowCache 已經(jīng)投入實(shí)際使用環(huán)境中,每次請(qǐng)求的量達(dá)到千級(jí)甚至萬(wàn)級(jí),總共緩存的量達(dá)到百萬(wàn)級(jí)別(后端使用 Hbase
作為最終的存儲(chǔ)方案,前端以 SlidingWindowCache 作為最前的緩存方案)。
SlidingWindowCache <https://github.com/hsxian/SlidingWindowCache>
項(xiàng)目剛剛起步,歡迎提出改進(jìn)意見。
熱門工具 換一換