.NET redis 客戶端開源組件 FreeRedis (繼 CSRedisCore 之後重寫)

什麼是 FreeRedis

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

FreeRedis 會嚴格按照 FreeSql 的開源方式,做好單元測試,兼容平台,簡單易用,有問必答,有求必應的態度,為中國 .NET 開源事業做一點點貢獻。

感謝大家的支援,項目還未公開就已經獲得 66 星。目前項目仍在起步階段,歡迎小夥伴參與進來,貢獻測試、或程式碼、或建議都可以。

項目當前的狀態:

  • 版本 0.0.8(目前不建議使用在生產環境)
  • 單元測試 268 個
  • 支援 集群、哨兵、主從(已通過測試)
  • 支援 連接池
  • 支援 .NET5/.NETCore 2.1+/.NET4.0+
  • 支援 Redis6.0 所有類型
  • 支援 Redis6.0 RESP3 協議
  • API 仍然與 redis-cli 命令保持一致
  • 採用最寬鬆的開源協議 MIT //github.com/2881099/FreeRedis

項目由來

說來話長,2016 年之前本人寫了一年多 nodejs 服務端應用,使用過 node-redis 組件,真心好用。在此期間有同事不停安利 .NET 可以跨平台了,勸我快回來搞 .NET,開始我是抗拒做螃蟹第一人的,不知道是哪天下午閑著蛋疼去體驗了一把 .NETCore 1.0-previewXX(不記得哪個版本了)。試了一把被吸引住了,體驗感受和 expressjs 像極了,再也看不見以往 webform/mvc 的缺點。

於是我準備入坑了,入坑第一件事除了 hello world,還需要做相關調研:

  • 性能OK
  • 設計OK
  • 發展OK(暫時的定級)
  • 相關組件OK(HttpClient、Redis、Ado.NET、等等基礎組件)

初始調研完成之後,接下下就要抽時間選型框架了,最終從眾多框架中選擇了合適團隊的一款://github.com/simplcommerce/SimplCommerce ,在這個項目原有基礎之上,結合企業規範要求訂製改造,大約兩個月時間完成了可生產的狀態。(框架不求開始盡善盡美,只求使用中不斷打磨,最終走向完美)

理想豐滿現實骨幹,接下來的故事就是遇到生產故障了,StackExchange.Redis、HttpClient 關於這兩個組件的問題,以前講過現在就不說了(萬萬沒想到這麼大的組件使用都能出現問題)。吃螃蟹就會掉坑,掉了坑就要想辦法解決,最終與 csredis 組件結緣。

以當時的情形縱觀 .NET 所有 redis 客戶端組件,只有 csredis 源碼最易改造支援 .NETCore(水平有限見諒),csredis 2014 年停止更新,本人於 2016 年將其改造支援 .NETCore 為主,以及增加連接池管理、集群、哨兵、redis2.8 以上的命令,在公司項目生產環境使用一年半載之後開源。

CSRedisCore 開源這到久,nuget 下載量達到 60W,收集需求若干,bug 若干(有解決了的、也有未能重現的),基於我已經對 redis 這塊很熟悉,然後 redis 5.0/6.0 又新增了蠻多特性,重新寫一款 bug 更少、可維護性更好的想法產生了。

經過幾個月的墨跡終於走通可用了,項目最終命名:FreeRedis


如何使用

🌈 Single machine redis (單機)

public static RedisClient cli = new RedisClient("127.0.0.1:6379,password=123,defaultDatabase=13");
//cli.Serialize = obj => JsonConvert.SerializeObject(obj);
//cli.Deserialize = (json, type) => JsonConvert.DeserializeObject(json, type);
cli.Notice += (s, e) => Console.WriteLine(e.Log); //print command log

cli.Set("key1", "value1");
cli.MSet("key1", "value1", "key2", "value2");

string value1 = cli.Get("key1");
string[] vals = cli.MGet("key1", "key2");

API 仍然與 redis-cli 命令保持一致,所以如果想了解 FreeRedis 每個方法怎麼使用,去百度搜索 「redis 命令」,有很多很多很多資料。don’t say so much!!!

支援 Redis6.0 支援的所有數據類型:strings, hashes, lists, sets, sorted sets, bitmaps, hyperloglogs, geo, streams And BloomFilter.

Parameter Default Explain
protocol RESP2 If you use RESP3, you need redis 6.0 environment
user <empty> Redis server username, requires redis-server 6.0
password <empty> Redis server password
defaultDatabase 0 Redis server database
max poolsize 100 Connection max pool size
min poolsize 5 Connection min pool size
idleTimeout 20000 Idle time of elements in the connection pool (MS)
connectTimeout 10000 Connection timeout (MS)
receiveTimeout 10000 Receive timeout (MS)
sendTimeout 10000 Send timeout (MS)
encoding utf-8 string charset
ssl false Enable encrypted transmission
name <empty> Connection name, use client list command to view

如果需要連接 IPv6,連接串請使用: [fe80::b164:55b3:4b4f:7ce6%15]:6379


🎣 Master-Slave (讀寫分離)

public static RedisClient cli = new RedisClient(
    "127.0.0.1:6379,password=123,defaultDatabase=13",
    "127.0.0.1:6380,password=123,defaultDatabase=13",
    "127.0.0.1:6381,password=123,defaultDatabase=13"
    );

var value = cli.Get("key1");

這樣創建的 cli,所有寫入命令都會連接 127.0.0.1:6379 執行,所有讀取命令只會隨機連接 127.0.0.1:6380 或 127.0.0.1:6381 執行。(內部已經為每個命令做好了讀寫標記)

⛳ Redis Sentinel (哨兵高可用)

public static RedisClient cli = new RedisClient(
    "mymaster,password=123", 
    new [] { "192.169.1.10:26379", "192.169.1.11:26379", "192.169.1.12:26379" },
    true //是否讀寫分離
    );

哨兵是一個分散式系統,為 Redis 提供高可用性解決方案,當主機 master 宕機之後,會馬上出現一個新的 master 可使用,確保服務的正常運作。

哨兵模式還可以設置讀寫分離,緩解 master 頻繁讀取數據的壓力,缺點:有可能讀到的數據不是最新,因為 redis 從 master 同步到 slave 有延時。

🌌 Redis Cluster (集群)

假如你有一個 Redis Cluster 集群,其中有三個主節點(7001-7003)、三個從節點(7004-7006),則連接此集群的程式碼:

public static RedisClient cli = new RedisClient(
    new ConnectionStringBuilder[] { "192.168.0.2:7001", "192.168.0.2:7001", "192.168.0.2:7003" }
    );

📡 Subscribe (訂閱)

using (cli.Subscribe("abc", ondata)) //wait .Dispose()
{
    Console.ReadKey();
}

void ondata(string channel, string data) =>
    Console.WriteLine($"{channel} -> {data}");

📃 Scripting (腳本)

var r1 = cli.Eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", 
    new[] { "key1", "key2" }, "first", "second") as object[];

var r2 = cli.Eval("return {1,2,{3,'Hello World!'}}") as object[];

cli.Eval("return redis.call('set',KEYS[1],'bar')", 
    new[] { Guid.NewGuid().ToString() })

💻 Pipeline (管道)

using (var pipe = cli.StartPipe())
{
    pipe.IncrBy("key1", 10);
    pipe.Set("key2", Null);
    pipe.Get("key1");

    object[] ret = pipe.EndPipe();
    Console.WriteLine(ret[0] + ", " + ret[2]);
}

// or Async Callback

using (var pipe = cli.StartPipe())
{
    var tasks = new List<Task>();
    long t0 = 0;
    task.Add(pipe.IncrByAsync("key1", 10).ContinueWith(t => t0 = t.Result)); //callback

    pipe.SetAsync("key2", Null);

    string t2 = null;
    task.Add(pipe.GetAsync("key1").ContinueWith(t => t2 = t.Result)); //callback

    pipe.EndPipe();
    Task.WaitAll(tasks.ToArray()); //wait all callback
    Console.WriteLine(t0 + ", " + t2);
}

📰 Transaction (事務)

using (var tran = cli.Multi())
{
    tran.IncrBy("key1", 10);
    tran.Set("key2", Null);
    tran.Get("key1");

    object[] ret = tran.Exec();
    Console.WriteLine(ret[0] + ", " + ret[2]);
}

// or Async Callback

using (var tran = cli.Multi())
{
    var tasks = new List<Task>();
    long t0 = 0;
    task.Add(tran.IncrByAsync("key1", 10).ContinueWith(t => t0 = t.Result)); //callback

    tran.SetAsync("key2", Null);

    string t2 = null;
    task.Add(tran.GetAsync("key1").ContinueWith(t => t2 = t.Result)); //callback

    tran.Exec();
    Task.WaitAll(tasks.ToArray()); //wait all callback
    Console.WriteLine(t0 + ", " + t2);
}

📯 GetDatabase (切庫)

using (var db = cli.GetDatabase(10))
{
    db.Set("key1", 10);
    var val1 = db.Get("key1");
}

結束語

目前項目仍在起步階段,歡迎小夥伴參與進來,貢獻測試、或程式碼、或建議都可以。

FreeRedis 使用最寬鬆的開源協議 MIT //github.com/2881099/FreeRedis

如果你有好的 redis 實現想法,歡迎給作者留言討論,謝謝觀看!