如何在 .NET 程式中使用 Redis 做為 Cache Server - Part 2 (使用 Hashes 型別)

如何在 .NET 程式中使用 Redis 做為 Cache Server 一文中把原本使用 .NET 的 MemoryCache 改為使用 Redis,其中用的是 Redis 最基本的型別 string ,今天將會用 Hash 型別來改寫,你可能跟我一樣想瞭解 Redis 各型別的 實際 差異,雖然官網上把各型別內容都清楚說明還附上 big O - O(1/n) 資訊,但我還是搞不清楚什麼情境要用哪個XD,所以打算來親身體驗一下其中差異,最後有空再來個大比較,今天就從最常被討論到的型別 Hash 開始

為什麼選擇 Hashes

Rico 大在 Redis(5)-好用的Hash 中提到兩個關鍵

  1. Hashes 有經過記憶體優化,效能是比較高的
  2. 不用像 string 會 lock 整個 entity ,可針對單一屬性更新

基本用法

  1. Hashes 的儲存方式是 key : HashEntry[]
  2. HashEntry 的內容是 key : value

如果有個 list ,而 list 中的物件又有好幾個屬性,你會不會跟我一樣懶得慢慢處理,甚至思考著是不是乾脆用 string 就好,反正 cache 本來就不是拿來常更新的嘛,但想要成為好工程師的大家怎麼可能真的這麼放棄了

如何改良

  1. 新增 abstract class
    • 準備讓其他 class 繼承用
  2. 在 abstract class 加上 ToRedisHash 方法
    • 讓其他 class 可以直接呼叫使用,減少重複的 code
  3. ToRedisHash 方法會回傳 KeyValuePair<string, HashEntry[]>
  4. 將需要 Redis Hash 的 class 繼承 abstract class
  5. 在需要 cache 的 class 上套用 Keyattribute 用來標記為 key
    • 需要 key-vlaue 的結構,所以需要有一個 key

範例程式碼

  1. abstract class

    public abstract class RedisHashExtension
    {
        public KeyValuePair<string, HashEntry[]> ToRedisHash()
        {
            object obj = this;
            var key = GetType().GetProperties().FirstOrDefault(prop => prop.IsDefined(typeof(KeyAttribute), false));
            if (key == null)
                throw new InvalidDataException("miss property with key attibute");
            var keystr = key.GetValue(obj).ToString();
            var props = GetType().GetProperties().Where(d => d.Name != key?.Name);
            List<HashEntry> hashentrys = new List<StackExchange.Redis.HashEntry>();
            foreach (var item in props)
            {
                hashentrys.Add(new HashEntry(item.Name.ToString(), item.GetValue(this)?.ToString()));
            }
    
            return new KeyValuePair<string, HashEntry[]>(keystr, hashentrys.ToArray());
        }
    }
    
  2. 需要 cache 的 class

    public class PersonInfo : RedisHashExtension
    {
        [Key]
        public Guid ID{ get; set; }
            
        public string Name { get; set; }
        
        public string Tel { get; set; }
    }
    
  3. RedisConnectionFactory

    private static readonly Lazy<ConnectionMultiplexer> Connection;
    static RedisConnectionFactory()
    {
        var connectionString = "localhost:6379";
        var options = ConfigurationOptions.Parse(connectionString);
        Connection = new Lazy<ConnectionMultiplexer>(() => ConnectionMultiplexer.Connect(options));
    }
    public static ConnectionMultiplexer GetConnection => Connection.Value;
    public static IDatabase RedisDB => GetConnection.GetDatabase();
    
  4. 實際使用

    IDatabase _db = RedisConnectionFactory.RedisDB;
    //製造假資料
    List<KeyValuePair<string, HashEntry[]>> people= new List<KeyValuePair<string, HashEntry[]>>();
    for (int i = 0; i < 3; i++)
    {
            people.Add((new Person { ID = Guid.NewGuid(), Name = $"{i}_yowko", Tel = $"{i}_0123456789"}).ToRedisHash());
    }
    //寫入 redis
    foreach (var item in people)
    {
        _db.HashSetAsync(item.Key,item.Value);    
    }
    //取資料
    _db.HashGet("{key}","{field}")
    

還可以更好嗎?

雖然 Hashes 結構比較好,但一筆一筆寫入的作法實在不合理,網路 io 可能就把改善的效能吃光了,這時候可以利用 Redis batch 的功能

  1. 建立 batch

    var batch = _db.CreateBatch();
    
  2. 加 cache 資料逐一加入 batch 中

    batch.HashSetAsync(item.Key,item.Value);
    
  3. 執行 batch

    batch.Execute();
    
  4. 範例程式碼

    IDatabase _db = RedisConnectionFactory.RedisDB;
    //製造假資料
    List<KeyValuePair<string, HashEntry[]>> people= new List<KeyValuePair<string, HashEntry[]>>();
    for (int i = 0; i < 3; i++)
    {
            people.Add((new Person { ID = Guid.NewGuid(), Name = $"{i}_yowko", Tel = $"{i}_0123456789"}).ToRedisHash());
    }
    //建立 batch
    var batch = _db.CreateBatch();
    foreach (var item in people)
    {
        //加到 batch 中
        batch.HashSetAsync(item.Key,item.Value);
    }
    //執行 batch 內容
    batch.Execute();
    

Hashes 指令介紹

指令參數對應 StackExchange.Redis 指令說明
HDELkey field [field …]HashDelete/HashDeleteAsync刪除一個或多個 hash 的field
HEXISTSkey fieldHashExists/HashExistsAsync判斷 field 是否存在於 hash 中
HGETkey fieldHashGet/HashGetAsync取得 hash(key) 中 field 的值
HGETALLkeyHashGetAll/HashGetAllAsync從 hash(key) 中讀取全部的 field 和 value
HINCRBYkey field incrementHashIncrement/HashIncrementAsync將 hash(key) 中指定 field 的值增加 increment
HINCRBYFLOATHashIncrement/HashIncrementAsynckey field increment將 hash(key) 中指定 field 的值增加 increment
HKEYSkeyHashKeys/HashKeysAsync取得 hash(key) 的所有 field
HLENkeyHashLength/HashLengthAsync取得 hash(key) 裡所有 field 的數量
HMGETkey field [field …]HashGetAll/HashGetAllAsync取得 hash(key) 裡面指定 field 的值
HMSETkey field value [field value …]HashSet/HashSetAsync設定 hash(key) 一個或多個 field 與 value
HSETkey field valueHashSet/HashSetAsync設定 hash(key) 一個 field 的值為 value
HSETNXkey field valueHashSet/HashSetAsync/
HashSetIfNotExistsAsync
設定 hash(key) 的一個 field 值為 value ,只有當這個 field 不存在時有效
HSTRLENkey field-取得 hash(key) 裡面指定 field 的長度
HVALSkeyHashValues/HashValuesAsync取得 hash(key) 的所有值
HSCANkey cursor [MATCH pattern] [COUNT count]HashScan逐一列出 hash(key) 裡面的元素

參考資料

  1. Redis(5)-好用的Hash
  2. StackExchange.Redis/MigratedBookSleeveTestSuite/Batches.cs