.Net 開源項目 FreeRedis 實現思路之 - Redis 6.0 客戶端緩存技術

寫在開頭

FreeRedis 是一款繼 CSRedisCore 以後重寫的 .NET redis 客戶端開源組件,以 MIT 協議開源託管於 github,目前支持 .NET 五、.NETCore 2.1+、.NETFramework 4.0+、Xamarin,有可能已經支持 AOT 編譯(目前未測試,但會往這個方向走)。git

  • 🌈 全部方法名與 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/FreeRedisgithub

瞭解 Redis

Redis是一個開源的使用C語言編寫、開源、支持網絡、可基於內存亦可持久化的日誌型、高性能的Key-Value數據庫,並提供多種語言的API。它一般被稱爲 數據結構服務器 ,由於值(value)能夠是 字符串(string)、哈希(map)、 列表(list)、集合(sets)、有序集合(sorted sets)、地理位置(Geo)、消息列隊(Streams)等類型。redis

與其餘 key - value 緩存產品有如下三個特色:數據庫

  • Redis支持數據的持久化,能夠將內存中的數據保持在磁盤中,重啓的時候能夠再次加載進行使用。
  • Redis不只僅支持簡單的key-value類型的數據,同時還提供list,set,zset,hash等數據結構的存儲。
  • Redis支持數據的備份,即master-slave模式的數據備份。

優點:json

  • 性能極高 – 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 中。c#

爲何須要客戶端緩存?緩存

咱們都知道,使用 Redis 進行數據的緩存的主要目的是減小對 MySQL 等數據庫的訪問,提供更快的訪問速度,畢竟 《Redis in Action》中提到的, Redis 的性能大體是普通關係型數據庫的 10 ~ 100 倍。bash

因此,以下圖所示,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 提供了三種模式通知客戶端,普通模式、廣播模式、轉發模式。

一、普通模式

普通模式依賴 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 了這種模式。


二、廣播模式

廣播模式和普通模式差很少,都須要依賴 RESP3.0 協議。這種方式下 Redis 服務端再也不消耗過多內存存儲信息,而是發送更多的失效消息給客戶端。

與普通模式必須獲取一次鍵的規則不一樣,廣播模式下,只要鍵被修改或刪除,符合規則的客戶端都會收到失效消息,並且是能夠屢次獲取的

與普通模式相比,雖然少存儲了一些數據,可是因爲須要對前綴規則進行匹配,會消耗必定的 CPU 資源,因此注意別使用過長的前綴。

廣播模式和普通模式同樣,須要解決命令同時讀取的問題(請見上面的兩段代碼)。


三、轉發模式

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 五、.NETCore 2.1+、.NETFramework 4.0+、Xamarin,有可能已經支持 AOT 編譯(目前未測試,但會往這個方向走)。

github: https://github.com/2881099/FreeRedis

謝謝支持!!

相關文章
相關標籤/搜索