65.9K
CodeProject 正在变化。 阅读更多。
Home

RedisProvider for .NET

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2020年6月2日

CPOL

8分钟阅读

viewsIcon

26319

downloadIcon

422

.NET Redis 容器和强类型数据对象

引言

已经有几个适用于 .NET 的 Redis 客户端库了——StackExchange.Redis、Microsoft.Extensions.Caching.RedisServiceStack.Redis,这些是最流行的几个——那么为什么还要写一个呢?我希望在 Redis 客户端库中实现一些功能:

  • 应用程序缓存的“模型”,类似于 EF 中的 DbContext
  • 自动处理 POCO 数据类型,并轻松支持其他数据原语
  • 帮助进行一致的键命名
  • 支持键“命名空间
  • 轻松识别键的类型和内容
  • Intellisense 只显示允许用于该键类型的命令

这些目标促成了名为 RedisContainer 的上下文/容器的设计,该容器包含模拟 Redis 键类型的强类型数据对象。RedisContainer 提供了键命名空间,并允许直观地模拟应用程序中使用的 Redis 键,并且可以选择性地跟踪使用的键,但本身不缓存任何数据。强类型对象也不在应用程序内存中缓存任何数据,而是封装了常见 数据类型 的特定命令

Redis 数据类型
RedisItem<T> 二进制安全字符串
RedisBitmap 位数组
RedisList<T> 列表
RedisSet<T> set
RedisSortedSet<T> zset
RedisHash<K, V> hash
RedisDtoHash<T> 将 hash 映射为 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");

任何类型的泛型参数可以是 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");

自动 JSON 序列化/反序列化 POCO 类型

  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(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 中的 列表 是根据插入顺序排序的元素集合。当列表项不是同一类型时,请使用 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 中的 集合 是唯一的、无序的元素集合。当集合项不是同一类型时,请使用 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
集合操作  
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
集合操作  
Sort SORT
SortAndStore SORT .. STORE
IntersectStore ZINTERSTORE
UnionStore ZUNIONSTORE
GetAsyncEnumerator() ZSCAN

RedisHash<TKey, TValue>

Redis HASH 是一个由与值关联的字段组成的映射。RedisHash<TKey, TValue> 将 hash 视为强类型键值对的字典。RedisValueHash 可用于在键和值中存储不同的数据类型,而 RedisDtoHash<TDto> 将 DTO 的属性映射到 hash 的字段。

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
Hash 操作  
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,但也可以轻松地扩展 RedisContainerpublic 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 是 hash,用作简单的字典,将用户名或身份验证“票证”字符串映射到用户 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);

现在是“动态”键。我们将为每个用户和帖子维护一个 hash,其键名包含 ID。KeyTemplate<T> 允许我们定义键类型和键名格式一次,然后根据需要检索单个键。这里的 hash 键也自动映射到 POCO/DTO 类型,其中 POCO 的属性是存储的 hash 中的字段。

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

最后,模型包含几个(ZSETs)排序集合的模板,它们由用户 ID 键控。

  // 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 一样,它要求您添加异步任务但不 await 它们,这通常会导致很多恼人的 CS4014 “因为这个调用没有被 await……”编译器警告。可以使用 pragma 禁用这些警告,但它们仍然可能使代码更容易出错。

其他 Redis 数据类型或功能——HyperLogLogs、GEO、streams 和 Pub/Sub——目前不支持。

我最初计划让这些强类型数据对象实现 .NET 接口,至少是IEnumerable<T>,以及根据情况实现IList<T>ISet<T>IDictionary<K, V>以提供熟悉的 .NET 语义。RedisProvider 的第一个版本只提供了同步 API,并且确实实现了 .NET 接口,但存在两个主要问题。首先,我发现 Redis 到 .NET 之间在 Redis 键类型支持的内容和看似互补的接口要求之间存在频繁的“阻抗不匹配”。其次,也是对我来说更重要的一点,鉴于我希望将 Intellisense 范围仅限于键类型可用的命令的目标,实现IEnumerable<T>或其任何子接口带来了大量的扩展方法(来自 System.Linq)。鉴于这些对象不本地存储数据,调用这些方法中的大多数将非常低效,并且不必要地混乱了 API 发现。

Github 存储库 在此

历史

  • 2020年6月2日:初始版本
© . All rights reserved.