.Net 開源項目 FreeRedis 實現思路之 – Redis 6.0 客戶端緩存技術
- 2020 年 11 月 24 日
- 筆記
- client-side-cahing, freeredis, Redis
寫在開頭
FreeRedis 是一款繼 CSRedisCore 之後重寫的 .NET redis 客戶端開源組件,以 MIT 協議開源託管於 github,目前支持 .NET 5、.NETCore 2.1+、.NETFramework 4.0+、Xamarin,有可能已經支持 AOT 編譯(目前未測試,但會往這個方向走)。
- 🌈 所有方法名與 redis-cli 保持一致
- 🌌 支持 Redis 集群(服務端要求 3.2 及以上版本)
- ⛳ 支持 Redis 哨兵模式
- 🎣 支持主從分離(Master-Slave)
- 📡 支持發佈訂閱(Pub-Sub)
- 📃 支持 Redis Lua 腳本
- 💻 支持管道(Pipeline)
- 📰 支持事務
- 🌴 支持 GEO 命令(服務端要求 3.2 及以上版本)
- 🌲 支持 STREAM 類型命令(服務端要求 5.0 及以上版本)
- ⚡ 支持本地緩存(Client-side-cahing,服務端要求 6.0 及以上版本)
- 🌳 支持 Redis 6 的 RESP3 協議
github: //github.com/2881099/FreeRedis
了解 Redis
Redis是一個開源的使用C語言編寫、開源、支持網絡、可基於內存亦可持久化的日誌型、高性能的Key-Value數據庫,並提供多種語言的API。它通常被稱為 數據結構服務器 ,因為值(value)可以是 字符串(string)、哈希(map)、 列表(list)、集合(sets)、有序集合(sorted sets)、地理位置(Geo)、消息列隊(Streams)等類型。
與其他 key – value 緩存產品有以下三個特點:
- Redis支持數據的持久化,可以將內存中的數據保持在磁盤中,重啟的時候可以再次加載進行使用。
- Redis不僅僅支持簡單的key-value類型的數據,同時還提供list,set,zset,hash等數據結構的存儲。
- Redis支持數據的備份,即master-slave模式的數據備份。
優勢:
- 性能極高 – Redis能讀的速度是110000次/s,寫的速度是81000次/s 。
- 豐富的數據類型 – Redis支持二進制案例的 Strings, Lists, Hashes, Sets, Ordered Sets, Geo, Streams 數據類型操作。
- 原子 – Redis的所有操作都是原子性的,同時Redis還支持對幾個操作全並後的原子性執行。
- 豐富的特性 – Redis還支持 publish/subscribe, 通知, key 過期等等特性。
本文看點
Redis 6.0 是一個可期的版本,增加了 RESP3.0 協議,ACL 權限控制,從原有的單線程改為多線程(性能提升2-3倍)等諸多更新。今天向大家介紹他的另一個重要特性:客戶端緩存技術,講解如何落設計在 .NET 中。
為什麼需要客戶端緩存?
我們都知道,使用 Redis 進行數據的緩存的主要目的是減少對 MySQL 等數據庫的訪問,提供更快的訪問速度,畢竟 《Redis in Action》中提到的, Redis 的性能大致是普通關係型數據庫的 10 ~ 100 倍。
所以,如下圖所示,Redis 用來存儲熱點數據,Redis 未命中,再去訪問數據庫,這樣可以應付大多數情況下的性能要求。
但是,Redis 也有其性能上限,並且訪問 Redis 必然有一定的網絡 I/O 以及序列化反序列化損耗。所以,往往會引入進程緩存,將最熱的數據存儲在本地,進一步加快訪問速度。
如上圖所示,Guava Cache 等進程緩存作為一級緩存,Redis 作為二級緩存:
- 先去 Guava Cache 中查詢數據,如果命中則直接返回。
- Guava Cache 中未命中,則再去 Redis 中查詢,如果命中則返回數據,並在 Guava Cache 中設置此數據。
- Redis 也未命中的話,只有去 MySQL 中查詢,然後依次將數據設置到 Redis 和 Guava Cache 中。
只使用 Redis 分佈式緩存時,遇到數據更新時,應用程序更新完 MySQL 中的數據,可以直接將 Redis 中對應緩存失效掉,保持數據的一致性。
而進程內緩存的數據一致性比分佈式的緩存面臨更大的挑戰。數據更新的時候,如何通知其他進程也更新自己的緩存呢?
如果按照分佈式緩存的思路,我們可以設置極短的緩存失效時間,這樣不必實現複雜的通知機制。
但是不同進程內的數據依然會面臨不一致的問題,並且不同進程緩存失效時間不統一,同一個請求到了不同的進程,可能出現反覆幻讀的情況。
落地分析
如上當 key 失效的時候,Redis 6.0 提供了三種模式通知客戶端,普通模式、廣播模式、轉發模式。
1、普通模式
普通模式依賴 RESP3.0 協議,需要在連接成功時使用 hello 命令開啟 RESP3.0 模式。
hello 3
client tracking on
落地在 .NET 之中時,我們必然是使用連接池技術,那麼每個連接都必須使用以上的兩個命令,此時每個連接是一個循環讀的操作,如下:
while (true)
{
var msg = await redisSocket.ReceiveAsync(); //等待 key 失效的通知
}
本來我們可以比較簡單的這樣執行命令:
await redisSocket.SendAsync("GET key1");
await redisSocket.ReceiveAsync(); //讀取響應的結果
可以看出來,兩段代碼同時讀,會導致讀取的結果錯亂。如何解決還需要三思,而我們 PASS 了這種模式。
2、廣播模式
廣播模式和普通模式差不多,都需要依賴 RESP3.0 協議。這種方式下 Redis 服務端不再消耗過多內存存儲信息,而是發送更多的失效消息給客戶端。
與普通模式必須獲取一次鍵的規則不同,廣播模式下,只要鍵被修改或刪除,符合規則的客戶端都會收到失效消息,而且是可以多次獲取的
與普通模式相比,雖然少存儲了一些數據,但是由於需要對前綴規則進行匹配,會消耗一定的 CPU 資源,所以注意別使用過長的前綴。
廣播模式和普通模式一樣,需要解決命令同時讀取的問題(請見上面的兩段代碼)。
3、轉發模式
Redis 為了兼容 RESP2 協議提供了轉發(Redirect)模式,不再使用 RESP3 原生支持 PUSH 消息,而是將消息通過 Pub/Sub 通知給另外一個客戶端,具體流程如下圖所示。
public void Start()
{
//訂閱 __redis__:invalidate
_sub = _cli.Subscribe("__redis__:invalidate", InValidate) as IPubSubSubscriber;
//攔截緩存
_cli.Interceptors.Add(() => new MemoryCacheAop(this));
//當網絡斷開的時候,清空本地緩存
_cli.Unavailable += (_, e) =>
{
lock (_dictLock) _dictSort.Clear();
_dict.Clear();
};
_cli.Connected += (_, e) =>
{
//最關鍵的一個命令,否則 __redis__:invalidate 無法收到訂閱消息
e.Client.ClientTracking(true, _sub.RedisSocket.ClientId, null, false, false, false, false);
};
}
void InValidate(string chan, object msg)
{
var keys = msg as object[];
foreach (var key in keys) //移除本地緩存
RemoveCache(string.Concat(key));
}
MemoryCacheAop 是 FreeRedis 已經實現好的攔截器,主要實現攔截命令執行,獲取本地內存。完整代碼://github.com/2881099/FreeRedis/blob/master/src/FreeRedis/ClientSideCaching.cs
測試功能
static Lazy<RedisClient> _cliLazy = new Lazy<RedisClient>(() =>
{
var r = new RedisClient("192.168.164.10:6379,database=1"); //redis 6.0
r.Serialize = obj => JsonConvert.SerializeObject(obj);
r.Deserialize = (json, type) => JsonConvert.DeserializeObject(json, type);
r.Notice += (s, e) => Console.WriteLine(e.Log);
return r;
});
static RedisClient cli => _cliLazy.Value;
static void Main(string[] args)
{
cli.UseClientSideCaching(new ClientSideCachingOptions
{
//本地緩存的容量
Capacity = 3,
//過濾哪些鍵能被本地緩存
KeyFilter = key => key.StartsWith("Interceptor"),
//檢查長期未使用的緩存
CheckExpired = (key, dt) => DateTime.Now.Subtract(dt) > TimeSpan.FromSeconds(2)
});
cli.Set("Interceptor01", "123123"); //redis-server
var val1 = cli.Get("Interceptor01"); //redis-server
var val2 = cli.Get("Interceptor01"); //本地
var val3 = cli.Get("Interceptor01"); //斷點等3秒,redis-server
cli.Set("Interceptor01", "234567"); //redis-server
var val4 = cli.Get("Interceptor01"); //redis-server
var val5 = cli.Get("Interceptor01"); //本地
var val6 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //redis-server
var val7 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //本地
var val8 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //本地
cli.MSet("Interceptor01", "Interceptor01Value", "Interceptor02", "Interceptor02Value", "Interceptor03", "Interceptor03Value"); //redis-server
var val9 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //redis-server
var val10 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //本地
//以下 KeyFilter 返回 false,從而不使用本地緩存
cli.Set("123Interceptor01", "123123"); //redis-server
var val11 = cli.Get("123Interceptor01"); //redis-server
var val12 = cli.Get("123Interceptor01"); //redis-server
var val23 = cli.Get("123Interceptor01"); //redis-server
Console.ReadKey();
}
cli.Notice 事件在控制台輸出內容:
Not connected 代表沒有經過 redis-server
192.168.164.10:6379 > CLIENT TRACKING ON REDIRECT 46
FreeRedis.RedisResult
(0ms)
192.168.164.10:6379 > SET Interceptor01 123123
OK
(24ms)
192.168.164.10:6379 > GET Interceptor01
123123
(2ms)
Not connected > GET Interceptor01
123123
(0ms)
192.168.164.10:6379 > GET Interceptor01
123123
(0ms)
192.168.164.10:6379 > SET Interceptor01 234567
OK
(0ms)
192.168.164.10:6379 > GET Interceptor01
234567
(0ms)
Not connected > GET Interceptor01
234567
(0ms)
192.168.164.10:6379 > MGET Interceptor01 Interceptor02 Interceptor03
[234567, Interceptor02Value, Interceptor03Value]
(0ms)
Not connected > MGET Interceptor01 Interceptor02 Interceptor03
[234567, Interceptor02Value, Interceptor03Value]
(0ms)
Not connected > MGET Interceptor01 Interceptor02 Interceptor03
[234567, Interceptor02Value, Interceptor03Value]
(0ms)
192.168.164.10:6379 > MSET Interceptor01 Interceptor01Value Interceptor02 Interceptor02Value Interceptor03 Interceptor03Value
False
(3ms)
192.168.164.10:6379 > MGET Interceptor01 Interceptor02 Interceptor03
[Interceptor01Value, Interceptor02Value, Interceptor03Value]
(1ms)
Not connected > MGET Interceptor01 Interceptor02 Interceptor03
[Interceptor01Value, Interceptor02Value, Interceptor03Value]
(0ms)
192.168.164.10:6379 > SET 123Interceptor01 123123
OK
(0ms)
192.168.164.10:6379 > GET 123Interceptor01
123123
(0ms)
192.168.164.10:6379 > GET 123Interceptor01
123123
(0ms)
192.168.164.10:6379 > GET 123Interceptor01
123123
(0ms)
寫在最後
FreeRedis 是一款繼 CSRedisCore 之後重寫的 .NET redis 客戶端開源組件,以 MIT 協議開源託管於 github,目前支持 .NET 5、.NETCore 2.1+、.NETFramework 4.0+、Xamarin,有可能已經支持 AOT 編譯(目前未測試,但會往這個方向走)。
github: //github.com/2881099/FreeRedis
謝謝支持!!