目錄
介紹
.NET已經有幾個Redis客戶端庫——tackExchange.Redis,Microsoft.Extensions.Caching.Redis,並且ServiceStack.Redis最受歡迎。它爲什麼還要編寫另一個庫?我想要Redis客戶端庫中的一些內容:
- 應用程序緩存的“模型”,類似於EF中的DbContext
- 自動處理POCO數據類型,並輕鬆支持其他數據原語
- 幫助一致的鍵命名
- 支持鍵“namespace”
- 輕鬆識別鍵的類型和內容
- Intellisense僅顯示鍵類型允許的命令
這些目標導致設計了一個名爲RedisContainer的上下文/容器,其中包含建模Redis鍵類型的強類型數據對象。RedisContainer提供了一個鍵命名空間,並允許在應用程序中使用一個直觀的Redis鍵模型,可以選擇跟蹤使用的鍵,但它本身不緩存任何數據。強類型對象也不會在應用程序內存中緩存任何數據,而是僅封裝特定於每種常見數據類型的命令:
類 |
Redis數據類型 |
RedisItem<T> |
二進制安全字符串 |
RedisBitmap |
位數組 |
RedisList<T> |
list |
RedisSet<T> |
set |
RedisSortedSet<T> |
zset |
RedisHash<K, V> |
hash |
RedisDtoHash<T> |
將哈希映射爲DTO |
RedisObject |
*所有鍵類型的基類 |
該庫依賴於StackExchange.Redis與Redis服務器的所有通信,並且該API僅支持異步I/O。
用法
基本
創建一個連接和容器。RedisConnection需要StackExchange配置字符串。RedisContainer對於所有鍵,需要一個連接和一個可選的名稱空間。
var cn = new RedisConnection("127.0.0.1:6379,abortConnect=false");
var container = new RedisContainer(cn, "test");
鍵由容器管理。該鍵可能已存在於Redis數據庫中。或沒有。該GetKey方法不調用Redis。如果容器正在跟蹤鍵創建並且鍵已經添加到容器中,則返回該對象,否則將創建並返回所請求類型的RedisObject的新鍵。
// A simple string key
var key1 = container.GetKey<RedisItem<string>>("key1");
// A key holding an integer.
var key2 = container.GetKey<RedisItem<int>>("key2");
對於任何類型的通用參數可以是一個IConvertible,byte[]或POCO/DTO。例:
var longitem = container.GetKey<RedisItem<long>>("longitem");
var intlist = container.GetKey<RedisList<int>("intlist");
var customers = container.GetKey<RedisHash<string, Customer>>("customers");
var cust1 = container.GetKey<RedisDtoHash<Customer>>("cust1");
POCO類型的自動JSON序列化/反序列化:
var key3 = container.GetKey<RedisItem<Customer>>("key3");
await key3.Set(new Customer { Id = 1, Name = "freddie" });
var aCust = await key3.Get();
所有鍵類型均支持基本命令:
key1.DeleteKey()
key1.Expire(30)
key1.ExpireAt(DateTime.Now.AddHours(1))
key1.IdleTime()
key1.KeyExists()
key1.Persist()
key1.TimeToLive()
訪問StackExchange.Redis.Database可以直接執行RedisProvider不支持的任何命令。例:
var randomKey = container.Database.KeyRandom();
模板化鍵創建
當使用在鍵名中包含對象ID的通用模式時,例如“user:1”或“user:1234”,手動創建每個鍵並確保數據類型和鍵名格式正確都是容易出錯的。KeyTemplate<T>充當指定類型和鍵名稱模式的密鑰的工廠
var docCreator = container.GetKeyTemplate<RedisItem<string>>("doc:{0}");
// Key name will be "doc:1"
var doc1 = docCreator.GetKey(1);
// Key name will be "doc:2"
var doc2 = docCreator.GetKey(2);
事務和批次
通過基於StackExchange.Redis的事務和批處理支持管道。使用RedisContainer來創建批處理或事務,然後使用WithBatch()或WithTransaction()添加排隊的任務。
// A simple batch
var key1 = container.GetKey<RedisSet<string>>("key1");
var batch = container.CreateBatch();
key1.WithBatch(batch).Add("a");
key1.WithBatch(batch).Add("b");
await batch.Execute();
// A simple transaction
var keyA = container.GetKey<RedisItem<string>>("keya");
var keyB = container.GetKey<RedisItem<string>>("keyb");
await keyA.Set("abc");
await keyB.Set("def");
var tx = container.CreateTransaction();
var task1 = keyA.WithTx(tx).Get();
var task2 = keyB.WithTx(tx).Get();
await tx.Execute();
var a = task1.Result;
var b = task2.Result;
或者,您可以使用以下所示的語法將任務直接添加到事務或批處理中:
var keyA = container.GetKey<RedisItem<string>>("keya");
var keyB = container.GetKey<RedisItem<string>>("keyb");
await keyA.Set("abc");
await keyB.Set("def");
var tx = container.CreateTransaction();
tx.AddTask(() => keyA.Get());
tx.AddTask(() => keyB.Get());
await tx.Execute();
var task1 = tx.Tasks[0] as Task<string>;
var task2 = tx.Tasks[1] as Task<string>;
var a = task1.Result;
var b = task2.Result;
強類型數據對象
RedisItem<T>和RedisBitmap
Redis二進制安全字符串。RedisBitmap是RedisItem<byte[]>添加位操作的操作。RedisValueItem是當通用參數類型不重要時可以使用的RedisItem<RedisValue>。
RedisItem <T> |
Redis命令 |
Get and set: |
|
Get(T) |
GET |
Set(T, [TimeSpan], [When]) |
SET,SETEX,SETNX |
GetSet(T) |
GETSET |
GetRange(long, long) |
GETRANGE |
SetRange(long, T) |
SETRANGE |
GetMultiple(IList<RedisItem<T>>) |
MGET |
SetMultiple(IList<KeyValuePair<RedisItem<T>, T>> |
MSET, MSETNX |
與字符串相關: |
|
Append(T) |
APPEND |
StringLength() |
STRLEN |
與數字有關: |
|
Increment([long]) |
INCR, INCRBY |
Decrement([long]) |
DECR, DECRBY |
RedisBitmap: |
|
GetBit(long) |
GETBIT |
SetBit(long, bool) |
SETBIT |
BitCount([long], [long]) |
BITCOUNT |
BitPosition(bool, [long], [long]) |
BITPOS |
BitwiseOp(Op, RedisBitmap, ICollection<RedisBitmap>) |
BITOP |
RedisList<T>
Redis中的LIST是元素的集合,按照插入的順序排序。當列表項不是同一類型時,使用RedisValueList。
RedisList<T> |
Redis命令 |
添加和刪除: |
|
AddBefore(T, T) |
LINSERT BEFORE |
AddAfter(T, T) |
LINSERT AFTER |
AddFirst(params T[]) |
LPUSH |
AddLast(params T[]) |
RPUSH |
Remove(T, [long]) |
LREM |
RemoveFirst() |
LPOP |
RemoveLast() |
RPOP |
索引訪問: |
|
First() |
LINDEX 0 |
Last() |
LINDEX -1 |
Index(long) |
LINDEX |
Set(long, T) |
LSET |
Range(long, long) |
LRANGE |
Trim(long, long) |
LTRIM |
雜項: |
|
Count() |
LLEN |
PopPush(RedisList<T>) |
RPOPLPUSH |
Sort |
SORT |
SortAndStore |
SORT .. STORE |
GetAsyncEnumerator() |
|
RedisSet<T>
Redis中的SET是獨一無二的集合,包含未排序的元素。當設置的項目不是同一類型時,使用RedisValueSet 。
RedisSet <T> |
Redis命令 |
添加和刪除: |
|
Add(T) |
SADD |
AddRange(IEnumerable<T>) |
SADD |
Remove(T) |
SREM |
RemoveRange(IEnumerable<T>) |
SREM |
Pop([long]) |
SPOP |
Peek([long]) |
SRANDMEMBER |
Contains(T) |
SISMEMBER |
Count() |
SCARD |
Set操作: |
|
Sort |
SORT |
SortAndStore |
SORT .. STORE |
Difference |
SDIFF |
DifferenceStore |
SDIFFSTORE |
Intersect |
SINTER |
IntersectStore |
SINTERSTORE |
Union |
SUNION |
UnionStore |
SUNIONSTORE |
雜項: |
|
ToList() |
SMEMBERS |
GetAsyncEnumerator() |
SSCAN |
RedisSortedSet<T>
Redis中的ZSET與SET相似,但是每個元素都有一個關聯的浮點值,稱爲score。當設置的項目不是同一類型時,使用RedisSortedValueSet。
RedisSortedSet <T> |
Redis命令 |
添加和刪除: |
|
Add(T, double) |
ZADD |
AddRange(IEnumerable<(T, double)>) |
ZADD |
Remove(T) |
ZREM |
RemoveRange(IEnumerable<(T, double)>) |
ZREM |
RemoveRangeByScore |
ZREMRANGEBYSCORE |
RemoveRangeByValue |
ZREMRANGEBYLEX |
RemoveRange([long], [long]) |
ZREMRANGEBYRANK |
範圍和計數: |
|
Range([long], [long], [Order]) |
ZRANGE |
RangeWithScores([long], [long], [Order]) |
ZRANGE ... WITHSCORES |
RangeByScore |
ZRANGEBYSCORE |
RangeByValue |
ZRANGEBYLEX |
Count() |
ZCARD |
CountByScore |
ZCOUNT |
CountByValue |
ZLEXCOUNT |
雜項: |
|
Rank(T, [Order]) |
ZRANK, ZREVRANK |
Score(T) |
ZSCORE |
IncrementScore(T, double) |
ZINCRBY |
Pop([Order]) |
ZPOPMIN, ZPOPMAX |
Set操作: |
|
Sort |
SORT |
SortAndStore |
SORT .. STORE |
IntersectStore |
ZINTERSTORE |
UnionStore |
ZUNIONSTORE |
GetAsyncEnumerator() |
ZSCAN |
RedisHash<TKey,TValue>
Redis HASH是由與值關聯的字段組成的映射。RedisHash<TKey, TValue>將哈希處理爲強類型鍵-值對的字典。RedisValueHash可以用於在鍵和值中存儲不同的數據類型,而RedisDtoHash<TDto>則將DTO的屬性映射到散列的字段。
RedisHash <TKey,TValue> |
Redis命令 |
獲取,設置和刪除: |
|
Get(TKey) |
HGET |
GetRange(ICollection<TKey>) |
HMGET |
Set(TKey, TValue, [When]) |
HSET, HSETNX |
SetRange(ICollection<KeyValuePair<TKey, TValue>>) |
HMSET |
Remove(TKey) |
HDEL |
RemoveRange(ICollection<TKey>) |
HDEL |
哈希操作: |
|
ContainsKey(TKey) |
HEXISTS |
Keys() |
HKEYS |
Values() |
HVALS |
Count() |
HLEN |
Increment(TKey, [long]) |
HINCRBY |
Decrement(TKey, [long]) |
HINCRBY |
雜項: |
|
ToList() |
HGETALL |
GetAsyncEnumerator() |
HSCAN |
RedisDtoHash <TDto> |
|
FromDto<TDto> |
HSET |
ToDto() |
HMGET |
示例應用程序
Redis文檔提供了一個簡單的Twitter克隆教程以及一個具有更完善應用程序的電子書。該示例基於其中描述的Redis概念。
示例“Twit”是一個非常基本的Blazor Webassembly應用程序。我們在這裏感興趣的部分是CacheService,它使用RedisProvider來建模和管理Redis緩存。
public class CacheService
{
private readonly RedisContainer _container;
private RedisItem<long> NextUserId;
private RedisItem<long> NextPostId;
private RedisHash<string, long> Users;
private RedisHash<string, long> Auths;
private RedisList<Post> Timeline;
private KeyTemplate<RedisDtoHash<User>> UserTemplate;
private KeyTemplate<RedisDtoHash<Post>> PostTemplate;
private KeyTemplate<RedisSortedSet<long>> UserProfileTemplate;
private KeyTemplate<RedisSortedSet<long>> UserFollowersTemplate;
private KeyTemplate<RedisSortedSet<long>> UserFollowingTemplate;
private KeyTemplate<RedisSortedSet<long>> UserHomeTLTemplate;
...
}
這裏的CacheService包含了RedisContainer,但它可以很容易地擴展RedisContainer而不是:public class CacheService : RedisContainer {}
在這兩種情況下,容器都將提供連接信息和keyNamespace,在這種情況下爲“twit”。容器創建的所有鍵名將採用“twit:{keyname}”格式。
在這裏,我們看到所謂的“固定”鍵,名稱爲常數的鍵和“動態”鍵(其名稱包含ID或其他變量數據)。
因此NextUserId,NextPostId簡單的“二進制安全字符串”項就是一個長整數。這些字段用於獲取新創建的用戶和帖子的ID:
NextUserId = _container.GetKey<RedisItem<long>>("nextUserId");
NextPostId = _container.GetKey<RedisItem<long>>("nextPostId");
var userid = await NextUserId.Increment();
var postid = await NextPostId.Increment();
Users和Auths是哈希,就像簡單的字典一樣,用於將用戶名或身份驗證“票證”字符串映射到用戶ID。
Users = _container.GetKey<RedisHash<string, long>>("users");
Auths = _container.GetKey<RedisHash<string, long>>("auths");
// Add a name-id pair
await Users.Set(userName, userid);
// Get a userid from a name
var userid = Users.Get(userName);
Timeline是Post POCO類型的列表。(該示例包括多個時間軸,通常以集合的形式存儲。這個列表與其說是有用的,不如說是說明問題的。)
Timeline = _container.GetKey<RedisList<Post>>("timeline");
var data = new Post {
Id = id, Uid = userid, UserName = userName, Posted = DateTime.Now, Message = message };
await Timeline.AddFirst(data);
現在爲“動態”鍵。我們將爲每個用戶和帖子維護一個散列,其鍵名包含ID。KeyTemplate<T>將允許我們定義鍵類型和鍵名曾經的格式,然後根據需要獲取單個鍵。此處的哈希鍵還自動映射到POCO/DTO類型,其中POCO的屬性是存儲的哈希中的字段。
UserTemplate = _container.GetKeyTemplate<RedisDtoHash<User>>("user:{0}");
PostTemplate = _container.GetKeyTemplate<RedisDtoHash<Post>>("post:{0}");
var user = UserTemplate.GetKey(userId);
var post = PostTemplate.GetKey(postId);
var userData = new User {
Id = userId, UserName = name, Signup = DateTime.Now, Password = pwd, Ticket = ticket
};
user.FromDto(userData);
var postData = new Post {
Id = postId, Uid = userid, UserName = userName, Posted = DateTime.Now, Message = message
};
post.FromDto(postData);
最後,該模型包含由用戶ID鍵入的幾個排序集(ZSET)的模板。
// The post ids of a user's posts:
UserProfileTemplate = _container.GetKeyTemplate<RedisSortedSet<long>>("profile:{0}");
// The user ids of a user's followers:
UserFollowersTemplate = _container.GetKeyTemplate<RedisSortedSet<long>>("followers:{0}");
// The user ids of who the user is following:
UserFollowingTemplate = _container.GetKeyTemplate<RedisSortedSet<long>>("following:{0}");
// The post ids of the posts in a user's timeline:
UserHomeTLTemplate = _container.GetKeyTemplate<RedisSortedSet<long>>("home:{0}");
有了這些,Id = 1的用戶將具有以下鍵:
RedisDtoHash<User>("user:1")
RedisSortedSet<long>("profile:1")
RedisSortedSet<long>("home:1")
RedisSortedSet<long>("following:1")
RedisSortedSet<long>("followers:1")
因此,這裏有一個簡單的模型,並且由於強類型的關鍵字段和模板而易於概念化。現在需要注意的是,RedisContainer將跟蹤這些鍵值,但是如果有很多鍵值—例如,數以千計的用戶和帖子—您可能不想讓容器維護所有這些鍵值的字典。
CacheService提供RegisterUser,LoginUser,CreatePost,GetTimeline和FollowUser等類似於上面提到的電子書中的功能,我把它們留給有興趣的人自己去探索。這是顯示RegisterUser邏輯的最後一個片段:
public async Task<string> RegisterUser(string name, string pwd)
{
if ((await Users.ContainsKey(name))) throw new Exception("User name already exists");
// Get the next user id
var id = await NextUserId.Increment();
// Get a RedisDtoHash<User>("user:{id}") key
var user = UserTemplate.GetKey(id);
// Populate a dto
var ticket = Guid.NewGuid().ToString();
var userData = new User {
Id = id, UserName = name, Signup = DateTime.Now, Password = pwd, Ticket = ticket };
// Create a transaction - commands will be sent and executed together
var tx = _container.CreateTransaction();
// -- populate user hash
user.WithTx(tx).FromDto(userData);
// -- add name-id pair
Users.WithTx(tx).Set(name, id);
// -- add ticket-id pair
Auths.WithTx(tx).Set(ticket, id);
// And now execute the transaction
await tx.Execute();
return ticket;
}
興趣點
爲什麼只異步?因爲I/O操作應該是異步的,並且Redis(和StackExchange.Redis)非常快,所以最好記住Redis不是本地進程內緩存。
API爲什麼不使用“*Async”命名方法?因爲我不喜歡他們。
RedisProvider中仍然存在一些痛點,但總的來說,我發現它是對基本StackExchange API的改進。事務(和批處理)的語法很笨拙,就像StackExchange要求您添加異步任務但不等待它們一樣,這經常導致CS4014令人煩惱的“因爲未等待此調用...”編譯器警告。可以通過編譯指示禁用這些功能,但仍然可以使代碼更易於出錯。
當前不支持其他Redis數據類型或功能——HyperLogLogs,GEO,streams和Pub / Sub。
我最初計劃讓這些強類型數據對象實現.NET接口,IEnumerable<T>至少,IList<T>,ISet<T>和IDictionary<K, V>作爲適當的提供熟悉的.NET語義。RedisProvider的第一個版本僅提供一個同步API並實現了.NET接口,但是存在兩個主要問題。首先,我發現Redis與.NET經常出現“阻抗不匹配”,這是Redis鍵類型支持的功能以及看似互補的接口所需要的。其次,鑑於我的目標是使Intellisense的範圍僅限於鍵類型可用的命令,對我來說更重要,這是System.Linq實現IEnumerable<T>時帶來的大量擴展方法或其任何子接口。鑑於這些對象不在本地保存數據,因此大多數方法如果被調用,效率將非常低下,並且會不必要地造成API查找混亂。
Github倉庫在這裏。