目录
介绍
用法
基本
模板化键创建
事务和批次
强类型数据对象
RedisItem和RedisBitmap
RedisList
RedisSet
RedisSortedSet
RedisHash,tvalue>
示例应用程序
兴趣点
介绍
.NET已经有几个Redis客户端库——tackExchange.Redis,Microsoft.Extensions.Caching.Redis,并且ServiceStack.Redis最受欢迎。它为什么还要编写另一个库?我想要Redis客户端库中的一些内容:
- 应用程序缓存的“模型”,类似于EF中的DbContext
- 自动处理POCO数据类型,并轻松支持其他数据原语
- 帮助一致的键命名
- 支持键“namespace”
- 轻松识别键的类型和内容
- Intellisense仅显示键类型允许的命令
这些目标导致设计了一个名为RedisContainer的上下文/容器,其中包含建模Redis键类型的强类型数据对象。RedisContainer提供了一个键命名空间,并允许在应用程序中使用一个直观的Redis键模型,可以选择跟踪使用的键,但它本身不缓存任何数据。强类型对象也不会在应用程序内存中缓存任何数据,而是仅封装特定于每种常见数据类型的命令:
| 类 | Redis数据类型 |
| RedisItem | 二进制安全字符串 |
| RedisBitmap | 位数组 |
| RedisList | list |
| RedisSet | set |
| RedisSortedSet | zset |
| RedisHash | hash |
| RedisDtoHash | 将哈希映射为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("key1");
// A key holding an integer.
var key2 = container.GetKey("key2");
对于任何类型的通用参数可以是一个IConvertible,byte[]或POCO/DTO。例:
var longitem = container.GetKey("longitem");
var intlist = container.GetKey keyA.Get());
tx.AddTask(() => keyB.Get());
await tx.Execute();
var task1 = tx.Tasks[0] as Task;
var task2 = tx.Tasks[1] as Task;
var a = task1.Result;
var b = task2.Result;
强类型数据对象
RedisItem和RedisBitmap
Redis二进制安全字符串。RedisBitmap是RedisItem添加位操作的操作。RedisValueItem是当通用参数类型不重要时可以使用的RedisItem。
| RedisItem | 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) | MGET |
| SetMultiple(IList | 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) | BITOP |
RedisList
Redis中的LIST是元素的集合,按照插入的顺序排序。当列表项不是同一类型时,使用RedisValueList。
| RedisList | 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) | RPOPLPUSH |
| Sort | SORT |
| SortAndStore | SORT .. STORE |
| GetAsyncEnumerator() |
|
RedisSet
Redis中的SET是独一无二的集合,包含未排序的元素。当设置的项目不是同一类型时,使用RedisValueSet 。
| RedisSet | Redis命令 |
| 添加和删除: |
|
| Add(T) | SADD |
| AddRange(IEnumerable) | SADD |
| Remove(T) | SREM |
| RemoveRange(IEnumerable) | 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
Redis中的ZSET与SET相似,但是每个元素都有一个关联的浮点值,称为score。当设置的项目不是同一类型时,使用RedisSortedValueSet。
| RedisSortedSet | Redis命令 |
| 添加和删除: |
|
| Add(T, double) | ZADD |
| AddRange(IEnumerable) | ZADD |
| Remove(T) | ZREM |
| RemoveRange(IEnumerable) | 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
Redis HASH是由与值关联的字段组成的映射。RedisHash将哈希处理为强类型键-值对的字典。RedisValueHash可以用于在键和值中存储不同的数据类型,而RedisDtoHash则将DTO的属性映射到散列的字段。
| RedisHash | Redis命令 |
| 获取,设置和删除: |
|
| Get(TKey) | HGET |
| GetRange(ICollection) | HMGET |
| Set(TKey, TValue, [When]) | HSET, HSETNX |
| SetRange(ICollection) | HMSET |
| Remove(TKey) | HDEL |
| RemoveRange(ICollection) | HDEL |
| 哈希操作: |
|
| ContainsKey(TKey) | HEXISTS |
| Keys() | HKEYS |
| Values() | HVALS |
| Count() | HLEN |
| Increment(TKey, [long]) | HINCRBY |
| Decrement(TKey, [long]) | HINCRBY |
| 杂项: |
|
| ToList() | HGETALL |
| GetAsyncEnumerator() | HSCAN |
| RedisDtoHash |
|
| FromDto | HSET |
| ToDto() | HMGET |
示例应用程序
Redis文档提供了一个简单的Twitter克隆教程以及一个具有更完善应用程序的电子书。该示例基于其中描述的Redis概念。
示例“Twit”是一个非常基本的Blazor Webassembly应用程序。我们在这里感兴趣的部分是CacheService,它使用RedisProvider来建模和管理Redis缓存。
public class CacheService
{
private readonly RedisContainer _container;
private RedisItem NextUserId;
private RedisItem NextPostId;
private RedisHash Users;
private RedisHash Auths;
private RedisList Timeline;
private KeyTemplate UserTemplate;
private KeyTemplate PostTemplate;
private KeyTemplate UserProfileTemplate;
private KeyTemplate UserFollowersTemplate;
private KeyTemplate UserFollowingTemplate;
private KeyTemplate UserHomeTLTemplate;
...
}
这里的CacheService包含了RedisContainer,但它可以很容易地扩展RedisContainer而不是:public class CacheService : RedisContainer {}
在这两种情况下,容器都将提供连接信息和keyNamespace,在这种情况下为“twit”。容器创建的所有键名将采用“twit:{keyname}”格式。
在这里,我们看到所谓的“固定”键,名称为常数的键和“动态”键(其名称包含ID或其他变量数据)。
因此NextUserId,NextPostId简单的“二进制安全字符串”项就是一个长整数。这些字段用于获取新创建的用户和帖子的ID:
NextUserId = _container.GetKey("nextUserId");
NextPostId = _container.GetKey("nextPostId");
var userid = await NextUserId.Increment();
var postid = await NextPostId.Increment();
Users和Auths是哈希,就像简单的字典一样,用于将用户名或身份验证“票证”字符串映射到用户ID。
Users = _container.GetKey("users");
Auths = _container.GetKey("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("timeline");
var data = new Post {
Id = id, Uid = userid, UserName = userName, Posted = DateTime.Now, Message = message };
await Timeline.AddFirst(data);
现在为“动态”键。我们将为每个用户和帖子维护一个散列,其键名包含ID。KeyTemplate将允许我们定义键类型和键名曾经的格式,然后根据需要获取单个键。此处的哈希键还自动映射到POCO/DTO类型,其中POCO的属性是存储的哈希中的字段。
UserTemplate = _container.GetKeyTemplate("user:{0}");
PostTemplate = _container.GetKeyTemplate("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("profile:{0}");
// The user ids of a user's followers:
UserFollowersTemplate = _container.GetKeyTemplate("followers:{0}");
// The user ids of who the user is following:
UserFollowingTemplate = _container.GetKeyTemplate("following:{0}");
// The post ids of the posts in a user's timeline:
UserHomeTLTemplate = _container.GetKeyTemplate("home:{0}");
有了这些,Id = 1的用户将具有以下键:
RedisDtoHash("user:1")
RedisSortedSet("profile:1")
RedisSortedSet("home:1")
RedisSortedSet("following:1")
RedisSortedSet("followers:1")
因此,这里有一个简单的模型,并且由于强类型的关键字段和模板而易于概念化。现在需要注意的是,RedisContainer将跟踪这些键值,但是如果有很多键值—例如,数以千计的用户和帖子—您可能不想让容器维护所有这些键值的字典。
CacheService提供RegisterUser,LoginUser,CreatePost,GetTimeline和FollowUser等类似于上面提到的电子书中的功能,我把它们留给有兴趣的人自己去探索。这是显示RegisterUser逻辑的最后一个片段:
public async Task 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:{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至少,IList,ISet和IDictionary作为适当的提供熟悉的.NET语义。RedisProvider的第一个版本仅提供一个同步API并实现了.NET接口,但是存在两个主要问题。首先,我发现Redis与.NET经常出现“阻抗不匹配”,这是Redis键类型支持的功能以及看似互补的接口所需要的。其次,鉴于我的目标是使Intellisense的范围仅限于键类型可用的命令,对我来说更重要,这是System.Linq实现IEnumerable时带来的大量扩展方法或其任何子接口。鉴于这些对象不在本地保存数据,因此大多数方法如果被调用,效率将非常低下,并且会不必要地造成API查找混乱。
Github仓库在这里。
