1. 程式人生 > >.Net Core 三大Redis客戶端對比和使用心得

.Net Core 三大Redis客戶端對比和使用心得

前言

  稍微複雜一點的網際網路專案,技術選型都可能會涉及Redis,.NetCore的生態越發完善,支援.NetCore的Redis客戶端越來越多,

下面三款常見的Redis客戶端,相信大家平時或多或少用到一些,結合平時對三款客戶端的使用,有些心得體會。

先比較巨集觀的背景: 

 

包名稱 背景 github star .NetStandard2.0目標框架上 依賴
Stackexchange.redis 老牌.Net Redis客戶端,免費無限制,Stackoverflow背書 3700
  • Pipelines.Sockets.Unofficial (>= 2.0.22)
  • System.Diagnostics.PerformanceCounter (>= 4.5.0)
  • System.IO.Pipelines (>= 4.5.1)
  • System.Threading.Channels
Microsoft.Extensions.Caching.StackExchangeRedis .Netcore 2.2針對IDistributedCache介面實現的Redis分散式快取  
  • Microsoft.Extensions.Caching.Abstractions (>= 2.2.0)
  • Microsoft.Extensions.Options (>= 2.2.0)
  • StackExchange.Redis 
CSRedisCore 國人實現的著名第三方客戶端, redis.io官方網站推薦 894
  • Newtonsoft.Json (>= 12.0.2)
  • SafeObjectPool (>= 2.1.1)
  • System.ValueTuple (>= 4.5.0)

使用心得

三款客戶端Redis支援的連線字串配置基本相同

  "connectionstrings": {
    "redis": "localhost:6379,password=abcdef,connectTimeout=5000,writeBuffer=40960"
  }

StackExchange.Redis

  定位是高效能、通用的Redis .Net客戶端;方便地應用Redis全功能;支援Redis Cluster

  • 高效能的核心在於 多路複用器(支援在多個呼叫執行緒高效共享Redis連線), 伺服器端操作使用ConnectionMultiplexer 類
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("server1:6379,server2:6379");
// 日常應用的核心類庫是IDatabase
IDatabase db = redis.GetDatabase();

// 支援Pub/Sub
ISubscriber sub = redis.GetSubscriber();
sub.Subscribe("messages", (channel, message) => {
    Console.WriteLine((string)message);
});
---
sub.Publish("messages", "hello");
也正是因為多路複用,StackExchange.Redis唯一不支援的Redis特性是 "blocking pops",這個特性是RedisMQ的關鍵理論。 如果你需要blocking pops, StackExchange.Redis官方推薦使用pub/sub模型模擬實現。
  • 日常操作的API請關注IDatabase介面,支援非同步方法,這裡我對【客戶端操作Redis儘量不要使用非同步方法】的說法不敢苟同,對於非同步方法我認為還是遵守微軟最佳實踐:對於IO密集的操作,能使用非同步儘量使用非同步
 _redisDB0.StringDecrementAsync("ProfileUsageCap", (double)1)      // 對應redis自增api:DECR mykey
 _redisDB0.HashGetAsync(profileUsage, eqidPair.ProfileId))       // 對應redis api: hget key field1
_redisDB0.HashDecrementAsync(profileUsage, eqidPair.ProfileId, 1)  //  對應redis雜湊自增api:  HINCRBY myhash field -1

 

  • ConnectionMultiplexer 方式支援隨時切換Redis DB,對於多個Redis DB的操作,我封裝了一個常用的Redis DB 操作客戶端。
 public class RedisStore
    {
        private static Lazy<ConnectionMultiplexer> LazyConnection;
        private static string connectionRedis = "localhost:6379";

        public RedisStore(string connectiontring)
        {
            connectionRedis = connectiontring ?? "localhost:6379";
            LazyConnection = new Lazy<ConnectionMultiplexer>(() => ConnectionMultiplexer.Connect(connectionRedis));
        }
        public static ConnectionMultiplexer Connection => LazyConnection.Value;
        public RedisDatabase RedisCache => new RedisDatabase(Connection);

    }

    public class RedisDatabase
    {
        private Dictionary<int, IDatabase> DataBases = new Dictionary<int, IDatabase>();
        
        public ConnectionMultiplexer RedisConnection { get; }

        public RedisDatabase(ConnectionMultiplexer Connection)
        {
            DataBases = new Dictionary<int, IDatabase>{ };
            for(var i=0;i<16;i++)
            {
                DataBases.Add(i, Connection.GetDatabase(i));
            }
            
            RedisConnection = Connection;
        }

        public IDatabase this[int index]
        {
            get
            {
                if (DataBases.ContainsKey(index))
                    return DataBases[index];
                else
                   return DataBases[0];
            }
        }
    }
RedisCache

 

Microsoft.Extensions.Caching.StackExchangeRedis

    從nuget doc可知,該元件庫依賴於 StackExchange.Redis 客戶端; 是.NetCore針對分散式快取提供的客戶端,側重點在 Redis的快取特性。

該庫是基於 IDistributedCache 介面實現的,該介面為實現分散式快取的通用性,快取內容將以byte[] 形式讀寫 ;另外能使用的函式簽名也更傾向於【通用的 增、查操作】
// add Redis cache service 
services.AddStackExchangeRedisCache(options =>
{
  options.Configuration = Configuration.GetConnectionString("redis");
  options.InstanceName = "SampleInstance";
});

// Set Cache Item (by byte[])
 lifetime.ApplicationStarted.Register(() =>
            {
                var currentTimeUTC = DateTime.UtcNow.ToString();
                byte[] encodedCurrentTimeUTC = Encoding.UTF8.GetBytes(currentTimeUTC);
                var options = new DistributedCacheEntryOptions()
                    .SetSlidingExpiration(TimeSpan.FromMinutes(20));
                cache.Set("cachedTimeUTC", encodedCurrentTimeUTC, options);
            });  

// Retrieve Cache Item
[HttpGet]
[Route("CacheRedis")]
public async Task<string> GetAsync()
{
            var ret = "";
            var bytes = await _cache.GetAsync("cachedTimeUTC");
            if (bytes != null)
            {
                ret = Encoding.UTF8.GetString(bytes);
                _logger.LogInformation(ret);
            }
            return  await Task.FromResult(ret);
}

① 很明顯,該Cache元件並不能做到自由切換 Redis DB, 目前可在redis連線字串一次性配置專案要使用哪個Redis DB

② 會在指定DB(預設為0)生成key = SampleInstancecachedTimeUTC 的redis快取項

③ Redis並不支援bytes[] 形式的儲存值,以上byte[] 實際是以Hash的形式儲存

 

 

 CSRedisCore

該元件的功能更為強大,針對實際Redis應用場景有更多玩法。 - 普通模式 - 官方叢集模式 redis cluster - 分割槽模式(作者實現)   普通模式使用方法極其簡單,這裡要提示的是: 該客戶端也不支援 隨意切換 Redis DB, 但是原作者給出一種緩解的方式:構造多客戶端。
var redisDB = new CSRedisClient[16];                        // 多客戶端
for (var a = 0; a < redisDB.Length; a++)
  redisDB[a] = new CSRedisClient(Configuration.GetConnectionString("redis") + ",defualtDatabase=" + a);

services.AddSingleton(redisDB);
// ----------------------------
_redisDB[0].IncrByAsync("ProfileUsageCap", -1)
_redisDB[0].HGetAsync(profileUsage, eqidPair.ProfileId.ToString())
_redisDB[0].HIncrByAsync(profileUsage, eqidPair.ProfileId.ToString(), -1);

 內建的靜態操作類RedisHelper, 與Redis-Cli 命令完全一致, 故他能原生支援”blocking pops”。

// 實現後臺服務,持續消費MQ訊息
public class BackgroundJob : BackgroundService
    {
        private readonly CSRedisClient[] _redisDB;
        private readonly IConfiguration _conf;
        private readonly ILogger _logger;
        public BackgroundJob(CSRedisClient[] csRedisClients,IConfiguration conf,ILoggerFactory loggerFactory)
        {
            _redisDB = csRedisClients;
            _conf = conf;
            _logger = loggerFactory.CreateLogger(nameof(BackgroundJob));
        }
        
        //  Background 需要實現的後臺任務
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _redisDB[0] = new CSRedisClient(_conf.GetConnectionString("redis") + ",defualtDatabase=" + 0);
            RedisHelper.Initialization(_redisDB[0]);

            while (!stoppingToken.IsCancellationRequested)
            {
                var key = $"eqidpair:{DateTime.Now.ToString("yyyyMMdd")}";
                // 阻塞式從右側讀取List首訊息 
                var eqidpair = RedisHelper.BRPop(5, key);
                // TODO Handler Message
                else
                    await Task.Delay(1000, stoppingToken);
            }
        }
    }

-----RedisMQ 生產者---
//  將一個或多個msg插入List頭部
RedisHelper.LPush(redisKey, eqidPairs.ToArray());
簡單有效RedisMQ

Redis的一點小經驗:

  • 對自己要使用的Redis API 的時間複雜度心裡要有數,儘量不要使用長時間執行的命令如keys *,可通過redis.io SlowLog命令觀測 哪些命令耗費較長時間

  • Redis Key可按照“:”分隔定義成有業務意義的字串,如NewUsers:201909:666666(某些Redis UI可直觀友好檢視該業務)

  • 合適確定Key-Value的大小:Redis對於small value更友好, 如果值很大,考慮劃分到多個key

  • 關於快取穿透,面試的時候會問,自行搜尋布隆過濾器。

  • redis雖然有持久化機制,但在實際中會將key-value 持久化到關係型資料庫,因為對於某些結構化查詢,SQL更為有效。

 

----- update 多說兩句-------

以上三大客戶端,Microsoft.Extensions.Caching.StackExchangeRedis 與其他兩者的定位還是有很大差距的,單純 使用Redis 快取特性, 有微軟出品,必屬精品情結的可使用此客戶端;

StackExchange.Redis、CSRedisCore 對於Redis全功能特性支援的比較全,但是我也始終沒有解決StackExchange.Redis : RedisTimeoutException 超時的問題,換成CSRedisCore 確實沒有出現Redis相關異常。