.NET的RedisProvider

目錄

介紹

用法

基本

模板化鍵創建

事務和批次

強類型數據對象

RedisItem和RedisBitmap

RedisList

RedisSet

RedisSortedSet

RedisHash,tvalue>

示例應用程序

興趣點


介紹

.NET已經有幾個Redis客戶端庫——tackExchange.RedisMicrosoft.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.RedisRedis服務器的所有通信,並且該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");

對於任何類型的通用參數可以是一個IConvertiblebyte[]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:1user: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二進制安全字符串RedisBitmapRedisItem<byte[]>添加位操作的操作。RedisValueItem是當通用參數類型不重要時可以使用的RedisItem<RedisValue>

RedisItem <T>

Redis命令

Get and set

 

Get(T)

GET

Set(T, [TimeSpan], [When])

SETSETEXSETNX

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中的ZSETSET相似,但是每個元素都有一個關聯的浮點值,稱爲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 <TKeyTValue>

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或其他變量數據)。

因此NextUserIdNextPostId簡單的二進制安全字符串項就是一個長整數。這些字段用於獲取新創建的用戶和帖子的ID

NextUserId = _container.GetKey<RedisItem<long>>("nextUserId");
NextPostId = _container.GetKey<RedisItem<long>>("nextPostId");

var userid = await NextUserId.Increment();
var postid = await NextPostId.Increment();

UsersAuths哈希,就像簡單的字典一樣,用於將用戶名或身份驗證票證字符串映射到用戶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);

TimelinePost 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);

現在爲動態鍵。我們將爲每個用戶和帖子維護一個散列,其鍵名包含IDKeyTemplate<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提供RegisterUserLoginUserCreatePostGetTimelineFollowUser等類似於上面提到的電子書中的功能,我把它們留給有興趣的人自己去探索。這是顯示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數據類型或功能——HyperLogLogsGEOstreamsPub / 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倉庫在這裏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章