Redis是一個開源的、高性能的、基於記憶體的鍵值資料庫,它支持多種數據結構,如字元串、列表、集合、散列、有序集合等。其中,Redis的散列(Hash)結構是一個常用的結構,今天跟大家分享一個我的日常操作,如何使用Redis的散列(Hash)結構來緩存和查詢對象的屬性值,以及如何用Lambda表達式樹 ...
Redis是一個開源的、高性能的、基於記憶體的鍵值資料庫,它支持多種數據結構,如字元串、列表、集合、散列、有序集合等。其中,Redis的散列(Hash)結構是一個常用的結構,今天跟大家分享一個我的日常操作,如何使用Redis的散列(Hash)結構來緩存和查詢對象的屬性值,以及如何用Lambda表達式樹來簡化這個過程。
一、什麼是Redis Hash結構
Redis Hash結構是一種鍵值對的集合,它可以存儲一個對象的多個欄位和值。例如,我們可以用一個Hash結構來存儲一個人的信息,如下所示:
HSET person:1 id 1 HSET person:1 name Alice HSET person:1 age 20
上面的命令將一個人的信息存儲到了一個名為person:1的Hash結構中,其中每個欄位都有一個名稱和一個值。我們可以使用HGET命令來獲取某個欄位的值,例如:
HGET person:1 name#Alice
我們也可以使用HGETALL命令來獲取所有欄位的值,例如:
HGETALL person:1id 1name Aliceage 20
二、如何使用C#來操作Redis Hash結構
為了在C#中操作Redis Hash結構,我們需要使用一個第三方庫:StackExchange.Redis。這個庫提供了一個ConnectionMultiplexer類,用於創建和管理與Redis伺服器的連接,以及一個IDatabase介面,用於執行各種命令。例如,我們可以使用以下代碼來創建一個連接對象和一個資料庫對象:
// 連接Redis伺服器 ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost"); // 獲取資料庫對象IDatabase db = redis.GetDatabase();
然後,我們可以使用db對象的HashSet方法和HashGet方法來存儲和獲取Hash結構中的欄位值。
// 創建一個HashEntry數組,存放要緩存的對象屬性 HashEntry[] hashfield = new HashEntry[3]; hashfield[0] = new HashEntry("id", "1"); hashfield[1] = new HashEntry("name", "Alice"); hashfield[2] = new HashEntry("age", "20"); // 使用HashSet方法將對象屬性緩存到Redis的散列(Hash)結構中 db.HashSet("person:1", hashfield); // 使用HashGetAll方法從Redis的散列(Hash)結構中查詢對象屬性 HashEntry[] result = db.HashGetAll("person:1"); // 遍歷結果數組,列印對象屬性 foreach (var item in result) { Console.WriteLine(item.Name + ": " + item.Value); }
但是,這種方式有一些缺點:
- 首先,我們需要手動將對象的屬性名和值轉換為HashEntry數組,並且保持一致性。
- 其次,我們需要使用字元串來指定要存儲或獲取的欄位名,並且還要避免拼寫錯誤或重覆。
- 最後,我們需要手動將返回的RedisValue類型轉換為我們需要的類型。
有沒有更優雅的方法來解決這個問題呢?答案是肯定的。
三、如何用Lambda表達式輕鬆操作Redis Hash結構
Lambda表達式是一種匿名函數,可以用來表示委托或表達式樹。在.NET中,我們可以使用Lambda表達式來操作實體類的屬性,比如獲取屬性的值或者更新屬性的值。
我們可以利用 Lambda表達式來指定要存儲或獲取的對象的屬性,而不是使用字元串。使用表達式樹來遍歷Lambda表達式,提取出屬性名和屬性值,並轉換為HashEntry數組或RedisValue數組,使其更易於使用。例如:
Get<Person>(p => new { p.Name, p.Age });
如果我們只想選擇一個屬性,就可以直接寫:
Get<Person>(p => p.Name)
如果要更新對象指定的屬性,可以這樣寫了:
Update<Person>(p => p .SetProperty(x => x.Name, "Alice") .SetProperty(x => x.Age, 25));
怎麼樣,這樣是不是優雅多了,這樣做有以下好處:
- 代碼更加可讀和可維護,因為我們可以直接使用對象的屬性,而不是使用字元串。
- 代碼更加穩定和精確,因為我們可以避免拼寫錯誤或重覆,並且可以利用編譯器的類型檢查和提示。
那麼,我們如何實現上面的方法呢?
1、Get方法
這個方法的目的是從緩存中獲取對象的一個或多個屬性值,使用一個泛型方法和一個Lambda表達式來實現。
private static TResult Get<T, TResult>(IDatabase db, int id, Expression<Func<T, TResult>> selector) { if (selector == null) throw new ArgumentNullException(nameof(selector)); // 使用擴展方法獲取要查詢的屬性名數組 var hashFields = selector.GetMemberNames().Select(m => new RedisValue(m)).ToArray(); // 從緩存中獲取對應的屬性值數組 var values = db.HashGet($"person:{id}", hashFields); // 使用擴展方法將HashEntry數組轉換為對象 var obj = values.ToObject<T>(hashFields); // 返回查詢結果 return selector.Compile()(obj); } private static TResult Get<TResult>(IDatabase db, int id, Expression<Func<Person, TResult>> selector) => Get<Person, TResult>(db, id, selector);
- 首先,定義一個泛型方法Get<T, TResult>,它接受一個資料庫對象db,一個對象id,和一個Lambda表達式selector作為參數。這個Lambda表達式的類型是Expression<Func<T, TResult>>,表示它接受一個T類型的對象,並返回一個TResult類型的結果。這個Lambda表達式的作用是指定要查詢的屬性。
- 然後,在Get<T, TResult>方法中,首先判斷selector是否為空,如果為空,則拋出異常。然後,使用擴展方法GetMemberNames來獲取selector中的屬性名數組,並轉換為RedisValue數組hashFields。這個擴展方法使用了ExpressionVisitor類來遍歷表達式樹,並重寫了VisitMember方法來獲取屬性名。接下來,使用db.HashGet方法從緩存中獲取對應的屬性值數組values,使用id作為鍵。然後,使用擴展方法ToObject來將values數組轉換為T類型的對象obj。這個擴展方法使用了反射來獲取T類型的屬性,並設置對應的屬性值和類型轉換。最後,返回selector編譯後並傳入obj作為參數的結果。
- 接下來,定義一個私有方法Get<TResult>,它接受一個資料庫對象db,一個對象id,和一個Lambda表達式selector作為參數。這個Lambda表達式的類型是Expression<Func<Person, TResult>>,表示它接受一個Person類型的對象,並返回一個TResult類型的結果。這個Lambda表達式的作用是指定要查詢的Person對象的屬性。
- 然後,在Get<TResult>方法中,直接調用Get<T, TResult>方法,並傳入db,id,selector作為參數,並指定T類型為Person。這樣,就可以得到一個TResult類型的結果。
2、MemberExpressionVisitor擴展類
這個類的作用是遍歷一個表達式樹,收集其中的成員表達式的名稱,並存儲到一個列表中。
public class MemberExpressionVisitor : ExpressionVisitor { private readonly IList<string> _names; public MemberExpressionVisitor(IList<string> list) { _names = list; } protected override Expression VisitMember(MemberExpression node) { var name = node.Member.Name; if (node.Expression is MemberExpression member) { Visit(member); name = member.Member.Name + "." + name; } _names.Add(name); return base.VisitMember(node); } }
- 首先,定義一個類MemberExpressionVisitor,它繼承自ExpressionVisitor類。這個類有一個私有欄位_names,用於存儲屬性名。它還有一個構造函數,接受一個IList<string>類型的參數list,並將其賦值給_names欄位。
- 然後,在MemberExpressionVisitor類中,重寫了VisitMember方法,這個方法接受一個MemberExpression類型的參數node。這個方法的作用是訪問表達式樹中的成員表達式節點,並獲取其屬性名。
- 接下來,在VisitMember方法中,首先獲取node節點的屬性名,並賦值給name變數。然後判斷node節點的表達式是否是另一個成員表達式,如果是,則遞歸地訪問該表達式,並將其屬性名和name變數用"."連接起來,形成一個屬性路徑。然後將name變數添加到_names集合中。最後返回基類的VisitMember方法的結果。
3、Update方法
這個方法目的是將一個對象指定的屬性名和值更新到緩存中,使用一個泛型方法和一個委托函數來實現。
public static Dictionary<string, object> Update<TSource>(Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>> setPropertyCalls) { if (setPropertyCalls == null) throw new ArgumentNullException(nameof(setPropertyCalls)); var nameValues = new Dictionary<string, object>(100); // 創建一個字典用於存儲屬性名和值 var calls = new SetPropertyCalls<TSource>(nameValues); // 創建一個SetPropertyCalls對象 setPropertyCalls(calls); // 調用傳入的函數,將屬性名和值添加到字典中 return nameValues; // 返回字典 } private static void Update(IDatabase db, int id, Func<SetPropertyCalls<Person>, SetPropertyCalls<Person>> setPropertyCalls) { var hashEntries = Update(setPropertyCalls) .Select(kv => new HashEntry(kv.Key, kv.Value != null ? kv.Value.ToString() : RedisValue.EmptyString)) .ToArray(); // 將HashEntry數組存儲到緩存中,使用對象的Id作為鍵 db.HashSet(id.ToString(), hashEntries); }}
- 首先,定義一個泛型方法Update<TSource>,它接受一個函數作為參數,這個函數的類型是Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>,表示它接受一個SetPropertyCalls<TSource>對象,並返回一個SetPropertyCalls<TSource>對象。這個函數的作用是設置要更新的屬性名和值。
- 然後,在Update<TSource>方法中,創建一個字典nameValues,用於存儲屬性名和值。創建一個SetPropertyCalls<TSource>對象calls,傳入nameValues作為構造參數。調用傳入的函數setPropertyCalls,並傳入calls作為參數。這樣,setPropertyCalls函數就可以通過調用calls的SetProperty方法來添加屬性名和值到nameValues字典中。最後,返回nameValues字典。
- 接下來,定義一個私有方法Update,它接受一個資料庫對象db,一個對象id,和一個函數setPropertyCalls作為參數。這個函數的類型是Func<SetPropertyCalls<Person>, SetPropertyCalls<Person>>,表示它接受一個SetPropertyCalls<Person>對象,並返回一個SetPropertyCalls<Person>對象。這個函數的作用是設置要更新的Person對象的屬性名和值。
- 然後,在Update方法中,調用Update(setPropertyCalls)方法,並傳入setPropertyCalls作為參數。這樣,就可以得到一個字典nameValues,包含了要更新的Person對象的屬性名和值。將nameValues字典轉換為HashEntry數組hashEntries,使用屬性值的字元串表示作為HashEntry的值。如果屬性值為空,則使用RedisValue.EmptyString作為HashEntry的值。最後,使用db.HashSet方法將hashEntries數組存儲到緩存中,使用id作為鍵。
4、SetPropertyCalls泛型類
這個類的作用是收集一個源對象的屬性名稱和值的對應關係,並提供一個鏈式調用的方法,用於設置屬性的值。
public class SetPropertyCalls<TSource> { private readonly Dictionary<string, object> _nameValues; public SetPropertyCalls(Dictionary<string, object> nameValues) { _nameValues = nameValues; } public SetPropertyCalls<TSource> SetProperty<TProperty>(Expression<Func<TSource, TProperty>> propertyExpression, TProperty valueExpression) { if (propertyExpression == null) throw new ArgumentNullException(nameof(propertyExpression)); if (propertyExpression.Body is MemberExpression member && member.Member is PropertyInfo property) { if (!_nameValues.TryAdd(property.Name, valueExpression)) { throw new ArgumentException($"The property '{property.Name}' has already been set."); } } return this; } }
- 首先,這個類有一個構造函數,接受一個Dictionary<string, object>類型的參數,作為存儲屬性名稱和值的對應關係的字典,並賦值給一個私有欄位_nameValues。
- 然後,這個類有一個泛型方法,叫做SetProperty。這個方法接受兩個參數,一個是表示源對象屬性的表達式,另一個是表示屬性值的表達式。
- 在這個方法中,首先判斷第一個參數是否為空,如果為空,則拋出ArgumentNullException異常。
- 然後判斷第一個參數的表達式體是否是一個成員表達式,並且該成員表達式的成員是否是一個屬性,如果是,則獲取該屬性的名稱,並賦值給一個局部變數property。
- 然後嘗試將該屬性名稱和第二個參數的值添加到_nameValues字典中,如果添加失敗,則說明該屬性已經被設置過了,拋出ArgumentException異常。
- 最後,返回當前對象的引用,實現鏈式調用的效果。
這樣,我們就可以得到一個包含所有要更新的屬性名和值的字典,然後我們就可以根據這些屬性名和值來更新實體類的屬性了。
Demo示例
讓我們來看一下代碼示例,為了方便演示和閱讀,這是臨時碼的,實際中大家可以根據自己習慣來進行封裝,簡化調用,同時也可以使用靜態字典來緩存編譯好的委托及對象屬性,提高性能。