Redis set集合
Redis set (集合)遵循无序排列的规则,集合中的每一个成员(也就是元素,叫法不同而已)都是字符串类型,并且不可重复。Redis set 是通过哈希映射表实现的,所以它的添加、删除、查找操作的时间复杂度为 O(1)。集合中最多可容纳 2^32 - 1 个成员(40 多亿个)。
Redis set 使用以下方式向集合中添加一个成员,语法格式如下:
127.0.0.1:6379> SADD key member [member ...]
- key:指定一个键
- member:集合中要存储的成员。
和其他数据类型一样,当集合中最后一个成员被删除时,存储成员所用的数据结构也会被自动删除。
集合有一个非常重要的特性就是“自动去重”,这使得它可以适用于许多场景,比如过滤掉已中奖用户的 id,保证该用户不会被第二次抽中。
认识set集合
1) intset
Redis set 采用了两种方式相结合的
底层存储结构,分别是 intset(整型数组)与 hash table(哈希表),当 set 存储的数据满足以下要求时,使用 intset 结构:
- 集合内保存的所有成员都是整数值;
- 集合内保存的成员数量不超过 512 个。
当不满足上述要求时,则使用 hash table 结构。
Redis 中 intset 的结构体定义如下:
typedf struct inset{
uint32_t encoding;//指定编码方式,默认为INSET_ENC_INT16
uint32_t length;//集合内成员的总个数
int8_t contents[];//实际存储成员的数组,并且数组中的数值从小到大依次排列
}inset;
- encoding:用来指定编码格式,共有三种,分别是 INTSET_ENC_INT16、INSET_ENC_INT32 和 INSET_ENC_INT64,它们对应不同的数值范围。Redis 为了尽可能地节省内存,它会根据插入数据的大小来选择不同的编码格式。
- length:集合内成员的数量,记录 contents 数组中共有多少个成员。
- contents:存储成员的数组,数组中的成员从小到大依次排列,且不允许重复。
intset 结构示意图如下所示:
在《Redis hash哈希散列(图解)》一节,我们已经对哈希表原理做了讲解, set 的哈希表与其相似,这里不再赘述。
命令汇总
命令 | 说明 |
---|---|
SADD key member1 [member2] | 向集合中添加一个或者多个元素,并且自动去重。 |
SCARD key | 返回集合中元素的个数。 |
SDIFF key1 [key2] | 求两个或多个集合的差集。 |
SDIFFSTORE destination key1 [key2] | 求两个集合或多个集合的差集,并将结果保存到指定的集合中。 |
SINTER key1 [key2] | 求两个或多个集合的交集。 |
SINTERSTORE destination key1 [key2] | 求两个或多个集合的交集,并将结果保存到指定的集合中。 |
SISMEMBER key member | 查看指定元素是否存在于集合中。 |
SMEMBERS key | 查看集合中所有元素。 |
SMOVE source destination member | 将集合中的元素移动到指定的集合中。 |
SPOP key [count] | 弹出指定数量的元素。 |
SRANDMEMBER key [count] | 随机从集合中返回指定数量的元素,默认返回 1个。 |
SREM key member1 [member2] | 删除一个或者多个元素,若元素不存在则自动忽略。 |
SUNION key1 [key2] | 求两个或者多个集合的并集。 |
SUNIONSTORE destination key1 [key2] | 求两个或者多个集合的并集,并将结果保存到指定的集合中。 |
SSCAN key cursor [match pattern] [count count] | 该命令用来迭代的集合中的元素。 |
命令演示
Redis 集合有特定的应用场景,比如用户的共同关注场景就可以使用 set 来实现。下面看一组示例,其中 user:1 与 user:2 代表两个用户,他们都关注了一些编程课程:
#创建集合并添加多个成员 127.0.0.1:6379> SADD user:1 python java mysql (integer) 3 127.0.0.1:6379> SADD user:2 python c redis (integer) 3 #对两个集合求交集,求出他们共同关注的编程技术 127.0.0.1:6379> SINTER user:1 user:2 1) "python" #两个集合求并集 127.0.0.1:6379> SUNION user:1 user:2 1) "java" 2) "python" 3) "mysql" 4) "redis" 5) "c" #查看集合所有成员 127.0.0.1:6379> SMEMBERS user:1 1) "mysql" 2) "java" 3) "python" #两个集合求并集,并把结果保存到另外一个user:3集合中 127.0.0.1:6379> SUNIONSTORE user:3 user:1 user:2 (integer) 5 #查看集合所有成员 127.0.0.1:6379> SMEMBERS user:3 1) "java" 2) "python" 3) "mysql" 4) "redis" 5) "c" #从集合中弹出一个元素 127.0.0.1:6379> SPOP user:1 1 1) "python" #从集合中弹出两个元素 127.0.0.1:6379> SPOP user:1 2 1) "mysql" 2) "java" #查看集合元素个数 127.0.0.1:6379> SCARD user:2 (integer) 3 #迭代集合中元素 127.0.0.1:6379> SSCAN user:3 0 1) "0" 2) 1) "mysql" 2) "redis" 3) "java" 4) "python" 5) "c"
在线练习工具:Try Redis
查看更多命令:Command reference – Redis
Redis zset有序集合
顾名思义,Redis zset(有序集合)中的成员是有序排列的,它和 set 集合的相同之处在于,集合中的每一个成员都是字符串类型,并且不允许重复;而它们最大区别是,有序集合是有序的,set 是无序的,这是因为有序集合中每个成员都会关联一个 double(双精度浮点数)类型的 score (分数值),Redis 正是通过 score 实现了对集合成员的排序。
zset 是 Redis 常用数据类型之一,它适用于排行榜类型的业务场景,比如 QQ 音乐排行榜、用户贡献榜等。在音乐排行榜单中,我们可以将歌曲的点击次数作为 score 值,把歌曲的名字作为 value 值,通过对 score 排序就可以得出歌曲“热度榜单”。
Redis 使用以下命令创建一个有序集合:
127.0.0.1:6379> ZADD key score member [score member ...]
key:指定一个键名;
score:分数值,用来描述 member,它是实现排序的关键;
member:要添加的成员(元素)。
当 key 不存在时,将会创建一个新的有序集合,并把分数/成员(score/member)添加到有序集合中;当 key 存在时,但 key 并非 zset 类型,此时就不能完成添加成员的操作,同时会返回一个错误提示。
注意:在有序集合中,成员是唯一存在的,但是分数(score)却可以重复。有序集合的最大的成员数为 2^32 - 1 (大约 40 多亿个)。
认识有序集合
1) 压缩列表
有序集合(zset)同样使用了两种不同的存储结构,分别是 zipList(压缩列表)和 skipList(跳跃列表),当 zset 满足以下条件时使用压缩列表:
- 成员的数量小于128 个;
- 每个 member (成员)的字符串长度都小于 64 个字节。
下面对压缩列表做简单介绍,它由以下五部分组成,如图所示:
上述每一部分在内存中都是紧密相邻的,并承担着不同的作用,介绍如下:
- zlbytes 是一个无符号整数,表示当前 ziplist 占用的总字节数;
- zltail 指的是压缩列表尾部元素相对于压缩列表起始元素的偏移量。
- zllen 指 ziplist 中 entry 的数量。当 zllen 比
2^16 - 2
大时,需要完全遍历 entry 列表来获取 entry 的总数目。 - entry 用来存放具体的数据项(score和member),长度不定,可以是字节数组或整数,entry 会根据成员的数量自动扩容。
- zlend 是一个单字节的特殊值,等于 255,起到标识 ziplist 内存结束点的作用。
下面执行ZADD
命令添加两个成员:xh(小红) 的工资是 3500.0;xm(小明) 的工资是 3200.0。
ZADD salary 3500.0 xh 3200.0 xm
上述成员在压缩列表中的布局,如下所示:
当 zset 使用压缩列表保存数据时,entry 的第一个节点保存 member,第二个节点保存 score。依次类推,集合中的所有成员最终会按照 score 从小到大排列。
2) 跳跃列表
当有序结合不满足使用压缩列表的条件时,就会使用 skipList 结构来存储数据。
跳跃列表(skipList)又称“跳表”是一种基于链表实现的随机化数据结构,其插入、删除、查找的时间复杂度均为 O(logN)。从名字可以看出“跳跃列表”,并不同于一般的普通链表,它的结构较为复杂,本节只对它做浅显的介绍,如有兴趣可自行研究。
在 Redis 中一个 skipList 节点最高可以达到 64 层,一个“跳表”中最多可以存储 2^64 个元素,每个节点都是一个 skiplistNode(跳表节点)。skipList 的结构体定义如下:
typedf struct zskiplist{
//头节点
struct zskiplistNode *header;
//尾节点
struct zskiplistNode *tail;
// 跳表中的元素个数
unsigned long length;
//表内节点的最大层数
int level;
}zskiplist;
- header:指向 skiplist 的头节点指针,通过它可以直接找到跳表的头节点,时间复杂度为 O(1);
- tail:指向 skiplist 的尾节点指针,通过它可以直接找到跳表的尾节点,时间复杂度为 O(1);
- length:记录 skiplist 的长度,也就跳表中有多少个元素,但不包括头节点;
- level:记录当前跳表内所有节点中的最大层数(level);
跳跃列表的每一层都是一个有序的链表,链表中每个节点都包含两个指针,一个指向同一层的下了一个节点,另一个指向下一层的同一个节点。最低层的链表将包含 zset 中的所有元素。如果说一个元素出现在了某一层,那么低于该层的所有层都将包含这个元素,也就说高层是底层的子集。
通过以下示意图进一步认识 skiplist 结构。下图是一个上下共四层的跳跃列表结构:
图1:跳跃列表示意图
跳跃列表中的每个节点都存储着 S:V(即 score/value),示意图显示了使用跳跃列表查找 S:V 节点的过程。跳跃列表的层数由高到低依次排列,最低层是 L0 层,最高层是 L3 层,共有 4 层。
图 1 所示,首先从最高层开始遍历找到第一个S:V
节点,然后从此节点开始,逐层下降,通过遍历的方式找出每一层的 S:V 节点,直至降至最底层(L0)才停止。在这个过程中找到所有 S:V 节点被称为期望的节点。跳跃列表把上述搜索一系列期望节点的过程称为“搜索路径”,这个“搜索路径”由搜索到的每一层的期望节点组成,其本质是一个列表。
常用命令汇总
命令 | 说明 |
---|---|
ZADD key score1 member1 [score2 member2] | 用于将一个或多个成员添加到有序集合中,或者更新已存在成员的 score 值 |
ZCARD key | 获取有序集合中成员的数量 |
ZCOUNT key min max | 用于统计有序集合中指定 score 值范围内的元素个数。 |
ZINCRBY key increment member | 用于增加有序集合中成员的分值。 |
ZINTERSTORE destination numkeys key [key ...] | 求两个或者多个有序集合的交集,并将所得结果存储在新的 key 中。 |
ZLEXCOUNT key min max | 当成员分数相同时,计算有序集合中在指定词典范围内的成员的数量。 |
ZRANGE key start stop [WITHSCORES] | 返回有序集合中指定索引区间内的成员数量。 |
ZRANGEBYLEX key min max [LIMIT offset count] | 返回有序集中指定字典区间内的成员数量。 |
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT] | 返回有序集合中指定分数区间内的成员。 |
ZRANK key member | 返回有序集合中指定成员的排名。 |
ZREM key member [member ...] | 移除有序集合中的一个或多个成员。 |
ZREMRANGEBYLEX key min max | 移除有序集合中指定字典区间的所有成员。 |
ZREMRANGEBYRANK key start stop | 移除有序集合中指定排名区间内的所有成员。 |
ZREMRANGEBYSCORE key min max | 移除有序集合中指定分数区间内的所有成员。 |
ZREVRANGE key start stop [WITHSCORES] | 返回有序集中指定区间内的成员,通过索引,分数从高到低。 |
ZREVRANGEBYSCORE key max min [WITHSCORES] | 返回有序集中指定分数区间内的成员,分数从高到低排序。 |
ZREVRANK key member | 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序。 |
ZSCORE key member | 返回有序集中,指定成员的分数值。 |
ZUNIONSTORE destination numkeys key [key ...] | 求两个或多个有序集合的并集,并将返回结果存储在新的 key 中。 |
ZSCAN key cursor [MATCH pattern] [COUNT count] | 迭代有序集合中的元素(包括元素成员和元素分值)。 |
基本命令演示
下面通过一组命令,让我们熟悉如何操作 zset 数据类型。以下示例是一个保存了员工薪水的有序集合:
#在有序集合中添加一个成员 127.0.0.1:6379> ZADD salary 4000 lucy (integer) 1 #同时添加多个成员 127.0.0.1:6379> ZADD salary 5000 tom 6000 Helen 6500.50 Jack 3000 Smith (integer) 4 #查询指定区间上的元素 127.0.0.1:6379> ZRANGE salary 0 4 1) "Smith" 2) "lucy" 3) "tom" 4) "Helen" 5) "Jack" #降序查看指定区间上的元素 127.0.0.1:6379> ZREVRANGE salary 0 4 1) "Jack" 2) "Helen" 3) "tom" 4) "lucy" 5) "Smith" #查看指定元素的分值 127.0.0.1:6379> ZSCORE salary lucy "4000" #查看所有元素和分值 127.0.0.1:6379> ZRANGE salary 0 4 WITHSCORES 1) "Smith" 2) "3000" 3) "lucy" 4) "4000" 5) "tom" 6) "5000" 7) "Helen" 8) "6000" 9) "Jack" 10) "6500.5" #统计指定工资范围内的元素个数3000<=score<=5000 127.0.0.1:6379> ZCOUNT salary 3000 5000 (integer) 3 #表示3000<score<5000 127.0.0.1:6379> ZCOUNT salary (3000 (5000 (integer) 1 #返回指定工资范围内的score和成员,限制条件是跳过1个元素,返回2个元素。 127.0.0.1:6379> ZRANGEBYSCORE salary 3000 6000 WITHSCORES LIMIT 1 2 1) "lucy" 2) "4000" 3) "tom" 4) "5000" #查看有序集合在指定字典区间内的成员的数 #其中 - 表示最小值,而 + 则表示最大值 127.0.0.1:6379> ZLEXCOUNT salary - + (integer) 5
在线练习工具:Try Redis
查看更多命令:Command reference – Redis