.Net Core使用分布式缓存Redis:数据结构

一、前言

本篇主要使用StackExchangeRedis在.Net Core中使用Redis,使用基础见:点击此处。

二、五种基础数据结构

1.字符串类型String

字符串类型是Redis中最基本的数据类型,它能存储任何形式的字符串,包括二进制数据。你可以用其存储用户的邮箱、JSON化的对象甚至是一张图片。一个字符串类型键允许存储地得数据的最大容量是512MB。
字符串类型是其他4种数据类型的基础,其他数据类型和字符串类型的差别从某种角度来说只是组织字符串的形式不同。例如,列表类型是以列表的形式组织字符串,二集合类型是以集合的形式组织字符串。
以下为StackExchangeRedis中字符串常用方法及其命令:
(1)赋值与取值

//方法
redisConnection.GetDatabase().StringSetAsync(key, value);
redisConnection.GetDatabase().StringGetAsync(key); 
//命令
127.0.0.1:6379> set stringKey stringValue
OK
127.0.0.1:6379> get stringKey
"stringValue"

(2)返回 key 中字符串值的子字符

//方法
redisConnection.GetDatabase().StringGetRangeAsync(key, start, end);
//命令
127.0.0.1:6379> getrange stringKey 6 10
"Value"

(3)将给定 key 的值设为 value ,并返回 key 的旧值(old value)

//方法
var oldvalue = await redisConnection.GetDatabase().StringGetSetAsync(key, oldkey);
//命令
127.0.0.1:6379> getset stringKey newValue
"stringValue"

(4)如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾

//方法
redisConnection.GetDatabase().StringAppendAsync(key, appendValue);
//命令
127.0.0.1:6379> append stringKey append
(integer) 14

2.散列类型Hash
Redis是采用字典结构以键值对的形式存储数据的,而Hash的键值也是一种字典结构,其存储了字段(field)和字段值的映射,但字段值只能是字符串,不支持其他数据类型,不能嵌套其他数据类型(其他数据类型同理)。
散列类型适合存储对象:使用对象类别和ID构成键名,使用字段表示对象的属性,而字段值则存储属性值。例如要存储ID为2的汽车对象,可以分别使用名为color、name和price的三个字段来存储该辆汽车的颜色、名称和价格。
以下为StackExchangeRedis中散列类型常用方法及其命令:
(1)赋值

//方法
HashEntry[] hashEntry = new HashEntry[] {
  new HashEntry("id",2),
  new HashEntry("color","red"),
  new HashEntry("price",200),
};
redisConnection.GetDatabase().HashSetAsync("youCar", hashEntry);
//命令
127.0.0.1:6379> hset myCar price 200
(integer) 1
127.0.0.1:6379> hset mCar color blue
(integer) 1
127.0.0.1:6379> hset mCar name tractor
(integer) 1

(2)取值

//方法
redisConnection.GetDatabase().HashGetAsync("youCar", "color");
redisConnection.GetDatabase().HashGetAllAsync("youCar");
//命令
127.0.0.1:6379> hget youCar color
"red"
127.0.0.1:6379> hgetall youCar
1) "id"
2) "2"
3) "color"
4) "red"
5) "price"
6) "200"

(3)获取所有散列类型中的字段

//方法
var keys = redisConnection.GetDatabase().HashKeys("youCar");
//命令
127.0.0.1:6379> hkeys youCar
1) "id"
2) "color"
3) "price"

3.列表List
列表类型(List)可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素,或者获得列表的一个片段。
列表类型内部是使用双向链表(double linked list)实现的,所以向列表两段添加元素的时间复杂都为O(1),获取越接近两端的元素速度就越快。这意味着即使是一个由几千万个元素的列表,获取头部或者尾部的10条记录也是极快的。
不过使用链表的代价是通过索引访问元素比较慢。于是,列表类型很适合于如社交网站的新鲜事、记录日志、消息队列等,类似获取最前几条的数据、从列表尾部插入等场景。
以下为StackExchangeRedis中列表类型常用方法及其命令:
(1)向列表两端增加元素

//方法
redisConnection.GetDatabase().ListLeftPushAsync("myList","head1");
redisConnection.GetDatabase().ListRightPush("myList","bottom1");
//命令
127.0.0.1:6379> lpush myList head1
(integer) 1127.0.0.1:6379> rpush myList bottom1
(integer) 2

(2)从列表两端移除并获取一个元素

//方法
var value = redisConnection.GetDatabase().ListLeftPopAsync("myList");
var value = redisConnection.GetDatabase().ListRightPopAsync("myList");
//命令
127.0.0.1:6379> lpop myList
"head1"
127.0.0.1:6379> rpop myList
"bottom1"
127.0.0.1:6379> rpop myList
(nil)

(3)获取列表中的元素片段(不删除只获取)

//方法
var value = redisConnection.GetDatabase().ListRangeAsync("myList",0,2);
//命令
127.0.0.1:6379> lrange myList 0 2
1) "1"
2) "2"
3) "3"

4.集合Set
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。常用的操作是向集合加入或者删除元素、判断某个元素是否存在等,由于Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
最方便的是多个集合类型键之间还可以进行并集、交集和差集运算。
可以运用在共同好友、喜好和好友推荐(交集超过阈值)等可以运用交差并集操作的场景。
以下为StackExchangeRedis中集合类型常用方法及其命令:
(1)增加/删除元素(可多个,相同元素自动忽略)

//方法
redisConnection.GetDatabase().SetAddAsync("mySet", new RedisValue[] { "a", "b" });
redisConnection.GetDatabase().SetRemoveAsync("mySet", "a");
//命令
127.0.0.1:6379> sadd mySet a
(integer) 1
127.0.0.1:6379> sadd mySet a b c
(integer) 2
127.0.0.1:6379> srem mySet c
(integer) 1
127.0.0.1:6379> srem mySet b a
(integer) 2

(2)获取交集

//方法
var value = redisConnection.GetDatabase().SetCombine(SetOperation.Intersect, "mySet1", "mySet2");
//命令
127.0.0.1:6379> sadd mySet1 a b c
(integer) 3
127.0.0.1:6379> sadd mySet2 b c d
(integer) 3
127.0.0.1:6379> sinter mySet1 mySet2
1) "c"
2) "b"

5.有序集合Sort Set
从有序集合的名字就可以看出它和集合的区别就是”有序“二字。
在集合类型的基础尚有序集合类型为集合中的每个元素都关联了一个分数,按分数大小进行从小到大的排序,这使得我们不仅可以完成插入、删除和判断元素是否在等集合类型支持的操作,还能够获得分数最高(或最低)的前N个元素、获得指定分数范围内的元素等与分数有关的操作。虽然集合中每个元素都是不同的,但是它们的分数却可以相同。
有序集合类型在某方面和列表类型有些相似

  • 二者都是有序。
  • 二者都可以获取某一范围的元素

但是二者由这很大的区别,这使得他们的应用场景有所不同。

  • 列表是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间元素的速度会较慢,所以它更加适合实现如“新鲜事”或“日志”这样很少访问中间元素的应用。
  • 有序集合是使用散列表和跳表(skip list)实现的,所以即使读取文娱中间部分的数据速度也快(时间复杂度是O(log(N)))。
  • 列表中不能简单地调整元素的位置,但有序集合可以(通过更改元素的分数),所以可以用于排行榜、权重应用(有权重的消息队列)。
  • 有序集合要比类别更耗内存。

以下为StackExchangeRedis中有序集合类型常用方法及其命令:
(1)增加元素

//方法
SortedSetEntry[] sortedSetEntry = new SortedSetEntry[] {
  new SortedSetEntry("tom",10),
  new SortedSetEntry("peter",20),
  new SortedSetEntry("david",30),
};
redisConnection.GetDatabase().SortedSetAddAsync("mySs", sortedSetEntry);
//命令
127.0.0.1:6379> zadd mySs 10 tom 20 peter 30 david
(integer) 3

(2)获取排名在某个分数范围的元素列表

//方法
var value = redisConnection.GetDatabase().SortedSetRemoveRangeByScoreAsync("mySs", 10, 20);
//命令
127.0.0.1:6379> zrangebyscore mySs 10 20
1) "tom"
2) "peter"

(3)增加某个元素的分数

//方法
var sorce = redisConnection.GetDatabase().SortedSetIncrementAsync("mySs", "top", 3);
//命令
127.0.0.1:6379> zincrby mySs 3 tom
"13"

6.小结

上面各类型所举得例子有限,基本所有的Redis命令在StackExchangeRedis中都有相应的异步和同步的方法,大家可以参考 https://redis.io/commands

三、其他数据结构

1.HyperLogLog

Redis 在 2.8.9 版本添加了 HyperLogLog 结构(其本身也是一种算法)。
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
所以主要应用于:

  • 统计访问IP数量;
  • 统计搜索关键词数量;
  • 统计用户在线数;
  • 等等大数量统计。

以下为StackExchangeRedis中常用方法及其命令:
(1)添加指定元素到 HyperLogLog 中

//方法
RedisValue[] redisValue = new RedisValue[] {
    110,120,130,130
};
redisConnection.GetDatabase().HyperLogLogAddAsync("key", redisValue);
//命令
127.0.0.1:6379> pfadd hyperlogKy 110 120 130 130
(integer) 1

(2)返回给定 HyperLogLog 的基数估算值。

//方法
var count = redisConnection.GetDatabase().HyperLogLogLength("key");
//命令
127.0.0.1:6379> pfcount hyperlogKy
(integer) 3

2.geo

Redis在3.2版本之后新增了一个geo(地理位置),其数据结构为有序集合sort set。
geo可以将地理位置信息(经纬度)储存起来,并计算两个地理坐标之间的位置、返回以指定位置为圆心指定半径内的所有地理位置信息等。
像是我们平时的打车、租房地图等功能中就可以用到。

既然前面说到geo是有序集合那它的score怎么来?是我们自己输入嘛?其实是通过我们存储的经纬度通过geohash计算后的base32编码字符串。geohash原理点击此处。
以下为StackExchangeRedis中常用方法及其命令:
(1)添加地理位置

//方法
GeoEntry[] geoEntry = new GeoEntry[] {
  new GeoEntry(120.20000, 30.26667, "hangzhou"),
  new GeoEntry(116.41667, 39.91667, "beijing"),
  new GeoEntry(121.47, 31.23, "shanghai"),
};
redisConnection.GetDatabase().GeoAdd("city", geoEntry);
//命令
127.0.0.1:6379> geoadd city 120.20000 30.26667 hangzhou  116.41667 39.91667 beijing 121.47 31.23 shanghai
(integer) 3

(2)获取geohash

//方法
var geohash = redisConnection.GetDatabase().GeoHash("city", "hanghzou");
//命令
127.0.0.1:6379> geohash city hangzhou
1) "wtmkpjyuph0"

(3)获取指定元素范围的地理信息位置集合

//方法
var geo = redisConnection.GetDatabase().GeoRadius("city", "hanghzou", 300, GeoUnit.Kilometers, 10, Order.Ascending, GeoRadiusOptions.WithGeoHash);
//命令
127.0.0.1:6379> georadiusbymember city hangzhou 300 km withcoord withdist withhash asc count 10
1) 1) "hangzhou"
   2) "0.0000"
   3) (integer) 4054134205316048
   4) 1) "120.20000249147415"
      2) "30.266670658987586"
2) 1) "shanghai"
   2) "161.9183"
   3) (integer) 4054803462927619
   4) 1) "121.47000163793564"
      2) "31.229999039757836"
在给定以下可选项时, 命令会返回额外的信息:

  • WITHDIST : 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
  • WITHCOORD : 将位置元素的经度和维度也一并返回。
  • WITHHASH : 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。
  • ASC : 根据中心的位置, 按照从近到远的方式返回位置元素。DESC : 根据中心的位置, 按照从远到近的方式返回位置元素。

它还有个类似的georadius命令,区别是由给定的经纬度为圆心。

(4)计算两个位置之间的距离

//方法
var geo = redisConnection.GetDatabase().GeoDistance("city", "hanghzou","beijing");
//命令
127.0.0.1:6379> geodist city hangzhou beijing km
"1126.8937"

3.发布订阅pub/sub 

“发布/订阅”模式中包含两种角色,分别是发布者和订阅者。订阅者可以订阅一个或者若干个频道(channel),而发布者可以向指定的频道发送消息,所有订阅此频道的订阅者都会收到此消息。
发布者通过publish命令发送消息:

//命令
127.0.0.1:6379> publish channel_1 hi
(integer) 0
//方法
var count = redisConnection.GetSubscriber().Publish("channel_1", "hi");

消息是发送出去了,publish命令的返回值表示接收到这条消息的订阅者数量。因为当前没有订阅这订阅这个频道,所以返回0。
注意,发送出去的消息是不会持久化的,订阅者只能收到订阅之后发布者发送的消息。
订阅者通过subscribe命令订阅一个或者多个频道:

//命令
127.0.0.1:6379> subscribe channel_1 channel_2
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel_1"
3) (integer) 1
1) "subscribe"
2) "channel_2"
3) (integer) 2
1) "message"
2) "channel_1"
3) "hi"
//方法
while (true)
{
  redisConnection.GetSubscriber().Subscribe("channel_1", (channel, message) =>
  {
    var msg = message;//收到的消息
    var chan = channel;//频道名称
  });
}

执行subscribe命令之后进入订阅状态,可能收到三种类型的回复。每种类型的回复都包含三个值,第一个值是消息的类型,根据消息类型的不同,第二、三个值得含义也不同。消息类型可能得取值有以下三个:

  • subscribe:表示订阅成功得反馈信息。第二个值是订阅成功得频道名称,第三个值是当前客户端订阅得频道数量。
  • message:这个类型得回复是我们最关心得,它表示接收到得消息。第二个值表示禅师消息的频道名称,第三个值是消息的内容。
  • unsubscribe:表示成功取消订阅某个频道。第二个值是对应频道的名称,第三个值是当前客户端订阅的频道数量。

StackExchangeRedis中用到message和unsubscribe这两种。
除了通过频道名之外,还可以使用psubscribe命令通过规则订阅频道。如约定规则为channel_?*则可以匹配channel_为开头的频道,如channel_1、channel_2等。

//命令
127.0.0.1:6379> psubscribe cahnnel_?*
Reading messages... (press Ctrl-C to quit)
(integer) 1
1) "psubscribe"
2) "cahnnel_?*"
3) (integer) 1
//方法
while (true)
{
  redisConnection.GetSubscriber().Subscribe("channel_?*", (channel, message) =>
  {
    var msg = message;//收到的消息
    var chan = channel;//频道名称
  });
}

解除订阅通过命令unsubscribe,如果不指定频道则取消全部订阅。在StackExchangeRedis中分别时方法Unsubscribe()和UnsubscribeAll()。