我們回顧一下上一篇文章中的內容,有一個朋友問我這樣一個問題:
我的業務依賴一些數據,因為數據庫訪問慢,我把它放在 Redis 里面,不過還是太慢了,有什么其它的方案嗎?
其實這個問題比較簡單的是吧?Redis 其實屬于網絡存儲,我對照下面的這個表格,可以很容易的得出結論,既然網絡存儲的速度慢,那我們就可以使用內存 RAM 存儲,把放 Redis 里面的數據給放內存里面就好了。
操作 | 速度 |
---|---|
執行指令 | 1/1,000,000,000 秒 = 1 納秒 |
從一級緩存讀取數據 | 0.5 納秒 |
分支預測失敗 | 5 納秒 |
從二級緩存讀取數據 | 7 納秒 |
使用 Mutex 加鎖和解鎖 | 25 納秒 |
從主存(RAM 內存)中讀取數據 | 100 納秒 |
在 1Gbps 速率的網絡上發送 2Kbyte 的數據 | 20,000 納秒 |
從內存中讀取 1MB 的數據 | 250,000 納秒 |
磁頭移動到新的位置(代指機械硬盤) | 8,000,000 納秒 |
從磁盤中讀取 1MB 的數據 | 20,000,000 納秒 |
發送一個數據包從美國到歐洲然后回來 | 150 毫秒 = 150,000,000 納秒 |
提出這個方案以后,接下來就遇到了另外一個問題:
但是數據比我應用的內存大,這怎么辦呢?
在上篇文章中,我們提到了使用 FASTER 作為內存+磁盤混合緩存的方案,但是由于 FASTER 的 API 比較難使用,另外在純內存場景中表現不如ConcurrentDictionary
,所以最后得出的結論也是僅供參考。
經過一段時間的研究,筆者實現了一個基于微軟 FasterKv 封裝的進程內混合緩存庫(內存+磁盤),它有著更加易用的 API,接下來就和大家討論討論它。
FasterKvCache 架構
這里需要簡單的說一說 FasterKvCache 的架構,它核心使用的 FasterKv,所以架構實際上和 FasterKv 一致,其原理比較復雜,所以筆者簡化了原理圖,大概就如下所示:

FasterKv 的熱數據會在內存中,而全量的數據會持久化在磁盤中。這中間有一些緩存淘汰算法,所以大家看到這張圖就能明白 FasterKvCache 適用和不適用哪些場景了。
如何使用它
筆者之前給 EasyCaching 提交了 FasterKv 的實現,但是由于有一些 EasyCaching 的高級功能在 FasterKv 上目前無法高性能的實現,所以單獨創建了這個庫,提供高性能和最基本的 API 實現;如果大家已經使用了 EasyCaching,那么可以直接使用 EasyCaching.FasterKv 這個 NuGet 包。
如果使用需要 FasterKvCache 的話,只需要安裝 Nuget 包,Nuget 包不同的功能如下所示,其中序列化包可以只安裝自己需要的即可。
軟件包名 | 版本 | 備注 |
---|---|---|
FasterKv.Cache.Core[1] | 1.0.0-rc1 | 緩存核心包,包含 FasterKvCache 主要的 API |
FasterKv.Cache.MessagePack[2] | 1.0.0-rc1 | 基于 MessagePack 的磁盤序列化包,它具有著非常好的性能,但是需要注意它稍微有一點使用門檻,大家可以看它的文檔。 |
FasterKv.Cache.SystemTextJson[3] | 1.0.0-rc1 | 基于 System.Text.Json 的磁盤序列化包,它是.NET 平臺上性能最好 JSON 序列化封裝,但是比 MessagePack 差。不過它易用性非常好,無需對緩存實體進行單獨配置。 |
使用
直接使用
我們可以直接通過new FasterKvCache(...)
的方式使用它,目前它只支持基本的三種操作Get
、Set
、Delete
。為了方便使用和性能的考慮,我們將 FasterKvCache 分為兩種 API 風格,一種是通用對象風格,一種是泛型風格。
通用對象:直接使用
new FasterKvCache(...)
創建,可以存放任意類型的 Value。它底層使用object
類型存儲,所以內存緩沖內訪問值類型對象會有裝箱和拆箱的開銷。泛型:需要使用
new FasterKvCache<T>(...)
創建,只能存放T
類型的 Value。它底層使用T
類型存儲,所以內存緩沖內不會有任何開銷。
當然如果內存緩沖不夠,對應的 Value 被淘汰到磁盤上,那么同樣都會有讀寫磁盤、序列化和反序列化開銷。
通用對象版本
代碼如下所示,同一個 cache 實例可以添加任意類型:
using?FasterKv.Cache.Core;
using?FasterKv.Cache.Core.Configurations;
using?FasterKv.Cache.MessagePack;//?create?a?FasterKvCache
var?cache?=?new?FasterKv.Cache.Core.FasterKvCache("MyCache",new?DefaultSystemClock(),new?FasterKvCacheOptions(),new?IFasterKvCacheSerializer[]{new?MessagePackFasterKvCacheSerializer{Name?=?"MyCache"}},null);var?key?=?Guid.NewGuid().ToString("N");//?sync
//?set?key?and?value?with?expiry?time
cache.Set(key,?"my?cache?sync",?TimeSpan.FromMinutes(5));//?get
var?result?=?cache.Get<string>(key);
Console.WriteLine(result);//?delete
cache.Delete(key);//?async
//?set
await?cache.SetAsync(key,?"my?cache?async");//?get
result?=?await?cache.GetAsync<string>(key);
Console.WriteLine(result);//?delete
await?cache.DeleteAsync(key);//?set?other?type?object
cache.Set(key,?new?DateTime(2022,2,22));
Console.WriteLine(cache.Get<DateTime>(key));
輸出結果如下所示:
my?cache?sync
my?cache?async
2022/2/22?0:00:00
泛型版本
泛型版本的話性能最好,但是它只允許添加一個類型,否則代碼將編譯不通過:
//?create?a?FasterKvCache<T>
//?only?set?T?type?value
var?cache?=?new?FasterKvCache<string>("MyTCache",new?DefaultSystemClock(),new?FasterKvCacheOptions(),new?IFasterKvCacheSerializer[]{new?MessagePackFasterKvCacheSerializer{Name?=?"MyTCache"}},null);
Microsoft.Extensions.DependencyInjection
當然,我們也可以直接使用依賴注入的方式使用它,用起來也非常簡單。按照通用和泛型版本的區別,我們使用不同的擴展方法即可:
var?services?=?new?ServiceCollection();
//?use?AddFasterKvCache
services.AddFasterKvCache(options?=>
{//?use?MessagePack?serializeroptions.UseMessagePackSerializer();
},?"MyKvCache");var?provider?=?services.BuildServiceProvider();//?get?instance?do?something
var?cache?=?provider.GetService<FasterKvCache>();
泛型版本需要調用相應的AddFasterKvCache<T>
方法:
var?services?=?new?ServiceCollection();
//?use?AddFasterKvCache<string>
services.AddFasterKvCache<string>(options?=>
{//?use?MessagePack?serializeroptions.UseMessagePackSerializer();
},?"MyKvCache");var?provider?=?services.BuildServiceProvider();//?get?instance?do?something
var?cache?=?provider.GetService<FasterKvCache<string>>();
配置
FasterKvCache 構造函數
public?FasterKvCache(string?name,?//?如果存在多個Cache實例,定義一個名稱可以隔離序列化等配置和磁盤文件ISystemClock?systemClock,?//?當前系統時鐘,new?DefaultSystemClock()即可FasterKvCacheOptions??options,?//?FasterKvCache的詳細配置,詳情見下文IEnumerable<IFasterKvCacheSerializer>??serializers,?//?序列化器,可以直接使用MessagePack或SystemTextJson序列化器ILoggerFactory??loggerFactory)?//?日志工廠?用于記錄FasterKv內部的一些日志信息
FasterKvCacheOptions 配置項
對于 FasterKvCache,有著和 FasterKv 差不多的配置項,更詳細的信息大家可以看FasterKv-Settings[4],下方是 FasterKvCache 的配置:
IndexCount:FasterKv 會維護一個 hash 索引池,IndexCount 就是這個索引池的 hash 槽數量,一個槽為 64bit。需要配置為 2 的次方。如 1024(2 的 10 次方)、 2048(2 的 11 次方)、65536(2 的 16 次方) 、131072(2 的 17 次方)。默認槽數量為 131072,占用 1024kb 的內存。
MemorySizeBit: FasterKv 用來保存 Log 的內存字節數,配置為 2 的次方數。默認為 24,也就是 2 的 24 次方,使用 16MB 內存。
PageSizeBit:FasterKv 內存頁的大小,配置為 2 的次方數。默認為 20,也就是 2 的 20 次方,每頁大小為 1MB 內存。
ReadCacheMemorySizeBit:FasterKv 讀緩存內存字節數,配置為 2 的次方數,緩存內的都是熱點數據,最好設置為熱點數據所占用的內存數量。默認為 20,也就是 2 的 20 次方,使用 16MB 內存。
ReadCachePageSizeBit:FasterKv 讀緩存內存頁的大小,配置為 2 的次方數。默認為 20,也就是 2 的 20 次方,每頁大小為 1MB 內存。
LogPath:FasterKv 日志文件的目錄,默認會創建兩個日志文件,一個以
.log
結尾,一個以obj.log
結尾,分別存放日志信息和 Value 序列化信息,注意,不要讓不同的 FasterKvCache 使用相同的日志文件,會出現不可預料異常。默認為{當前目錄}/FasterKvCache/{進程 Id}-HLog/{實例名稱}.log。SerializerName:Value 序列化器名稱,需要安裝序列化 Nuget 包,如果沒有單獨指定
Name
的情況下,可以使用MessagePack
和SystemTextJson
。默認無需指定。ExpiryKeyScanInterval:由于 FasterKv 不支持過期刪除功能,所以目前的實現是會定期掃描所有的 key,將過期的 key 刪除。這里配置的就是掃描間隔。默認為 5 分鐘。
CustomStore:如果您不想使用自動生成的實例,那么可以自定義的 FasterKv 實例。默認為 null。
所以 FasterKvCache 所占用的內存數量基本就是(IndexCount*64)+(MemorySize)+ReadCacheMemorySize
,當然如果 Key 的數量過多,那么還有加上OverflowBucketCount * 64
。
容量規劃
從上面提到的內容大家可以知道,FasterKvCache 所占用的內存字節基本就是(IndexCount * 64)+(MemorySize) + ReadCacheMemorySize + (OverflowBucketCount * 64)
。磁盤的話就是保存了所有的數據+對象序列化的數據,由于不同的序列化協議有不同的大小,大家可以先進行測試。
內存數據存儲到 FasterKv 存儲引擎,每個 key 都會額外元數據信息,存儲空間占用會有一定的放大,建議在磁盤空間選擇上,留有適當余量,按實際存儲需求的 1.2 - 1.5 倍預估。
如果使用內存存儲 100GB 的數據,總的訪問 QPS 不到 2W,其中 80%的數據都很少訪問到。那么可以使用 【32GB 內存 + 128GB 磁盤】 存儲,節省了近 70GB 的內存存儲,內存成本可以下降 50%+。
性能
目前作者還沒有時間將 FasterKvCache 和其它主流的緩存庫進行比對,現在只對 FasterKvCache、EasyCaching.FasterKv 和 EasyCaching.Sqlite 做的比較。下面是 FasterKVCache 的配置,總占用約為 2MB。
services.AddFasterKvCache<string>(options?=>
{options.IndexCount?=?1024;options.MemorySizeBit?=?20;options.PageSizeBit?=?20;options.ReadCacheMemorySizeBit?=?20;options.ReadCachePageSizeBit?=?20;//?use?MessagePack?serializeroptions.UseMessagePackSerializer();
},?"MyKvCache");
由于作者筆記本性能不夠,使用 Sqlite 無法在短期內完成 100W、1W 個 Key 的性能測試,所以我們在默認設置下將數據集大小設置為 1000 個 Key,設置 50%的熱點 Key。進行 100%讀、100%寫和 50%讀寫隨機比較。
可以看到無論是讀、寫還是混合操作 FasterKvCache 都有著不俗的性能,在 8 個線程情況下,TPS 達到了驚人的 1600w/s。
緩存 | 類型 | 線程數 | Mean(us) | Error(us) | StdDev(us) | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|---|---|
fasterKvCache | Read | 8 | 59.95 | 3.854 | 2.549 | 1.5259 | 7.02 | NULL |
fasterKvCache | Write | 8 | 63.67 | 1.032 | 0.683 | 0.7935 | 3.63 | NULL |
fasterKvCache | Random | 4 | 64.42 | 1.392 | 0.921 | 1.709 | 8.38 | NULL |
fasterKvCache | Read | 4 | 64.67 | 0.628 | 0.374 | 2.5635 | 11.77 | NULL |
fasterKvCache | Random | 8 | 64.80 | 3.639 | 2.166 | 1.0986 | 5.33 | NULL |
fasterKvCache | Write | 4 | 65.57 | 3.45 | 2.053 | 0.9766 | 4.93 | NULL |
fasterKv | Read | 8 | 92.15 | 10.678 | 7.063 | 5.7373 | - | 26.42 KB |
fasterKv | Write | 4 | 99.49 | 2 | 1.046 | 10.7422 | - | 49.84 KB |
fasterKv | Write | 8 | 108.50 | 5.228 | 3.111 | 5.6152 | - | 25.93 KB |
fasterKv | Read | 4 | 109.37 | 1.476 | 0.772 | 10.9863 | - | 50.82 KB |
fasterKv | Random | 8 | 119.94 | 14.175 | 9.376 | 5.7373 | - | 26.18 KB |
fasterKv | Random | 4 | 124.31 | 6.191 | 4.095 | 10.7422 | - | 50.34 KB |
fasterKvCache | Read | 1 | 207.77 | 3.307 | 1.73 | 9.2773 | 43.48 | NULL |
fasterKvCache | Random | 1 | 208.71 | 1.832 | 0.958 | 6.3477 | 29.8 | NULL |
fasterKvCache | Write | 1 | 211.26 | 1.557 | 1.03 | 3.418 | 16.13 | NULL |
fasterKv | Write | 1 | 378.60 | 17.755 | 11.744 | 42.4805 | - | 195.8 KB |
fasterKv | Read | 1 | 404.57 | 17.477 | 11.56 | 43.457 | - | 199.7 KB |
fasterKv | Random | 1 | 441.22 | 14.107 | 9.331 | 42.9688 | - | 197.75 KB |
sqlite | Read | 8 | 7450.11 | 260.279 | 172.158 | 54.6875 | 7.8125 | 357.78 KB |
sqlite | Read | 4 | 14309.94 | 289.113 | 172.047 | 109.375 | 15.625 | 718.9 KB |
sqlite | Read | 1 | 56973.53 | 1,774.35 | 1,173.62 | 400 | 100 | 2872.18 KB |
sqlite | Random | 8 | 475535.01 | 214,015.71 | 141,558.14 | - | - | 395.15 KB |
sqlite | Random | 4 | 1023524.87 | 97,993.19 | 64,816.43 | - | - | 762.46 KB |
sqlite | Write | 8 | 1153950.84 | 48,271.47 | 28,725.58 | - | - | 433.7 KB |
sqlite | Write | 4 | 2250382.93 | 110,262.72 | 72,931.96 | - | - | 867.7 KB |
sqlite | Write | 1 | 4200783.08 | 43,941.69 | 29,064.71 | - | - | 3462.89 KB |
sqlite | Random | 1 | 5383716.10 | 195,085.96 | 129,037.28 | - | - | 2692.09 KB |
總結
可以看到 FasterKvCache 有著不俗的性能,目前也在筆者朋友的項目使用上了,反饋不錯,解決了他的緩存問題。由于現在還只是 1.0.0-rc1 版本,還有很多特性沒有實現。可能有一些 BUG 還存在,歡迎大家試用和反饋問題。
Github 開源地址: https://github.com/InCerryGit/FasterKvCache
參考鏈接
https://developer.aliyun.com/article/740811
參考資料
[1]
FasterKv.Cache.Core: https://www.nuget.org/packages/FasterKv.Cache.Core
[2]FasterKv.Cache.MessagePack: https://www.nuget.org/packages/FasterKv.Cache.MessagePack
[3]FasterKv.Cache.SystemTextJson: https://www.nuget.org/packages/FasterKv.Cache.SystemTextJson
[4]FasterKv-Settings: https://microsoft.github.io/FASTER/docs/fasterkv-basics/#fasterkvsettings