寫在開頭
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: https://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 已經實現好的攔截器,主要實現攔截命令執行,獲取本地記憶體。完整程式碼:https://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: https://github.com/2881099/FreeRedis
謝謝支援!!