一、Set 集合
集合类型也是保存多个字符串类型的元素的(可以使用 json 格式让 string 也能存储结构化数据),但和列表类型不同的是,集合中:
- 元素之间是无序的。(此处的 “无序” 是和 list 的有序相对应的)
- 元素不允许重复,如下图所示。
集合类型:
一个集合中最多可以存储个元素。Redis 除了支持集合内的增删查改操作,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多问题。
- list:[1, 2, 3] 和 [2, 1, 3] 是两个不同的 list
- set:[1, 2, 3] 和 [2, 1, 3] 是同一个集合
1、普通命令
(1)SADD
将⼀个或者多个元素添加到 set 中。
注意:重复的元素无法添加到 set 中。
语法:
SADD key member [member ...]
命令有效版本:
1.0.0 之后
时间复杂度:
O(1)
返回值:
本次添加成功的元素个数。
示例:
(2)SMEMBERS
获取一个 set 中的所有元素,注意,元素间的顺序是无序的。
语法:
SMEMBERS key
命令有效版本:
1.0.0 之后
时间复杂度:
O(N),N 是集合中的元素个数。
返回值:
所有元素的列表。
示例:
(3)SISMEMBER
判断一个元素在不在 set 中。
语法:
SISMEMBER key member
命令有效版本:
1.0.0 之后
时间复杂度:
O(1)
返回值:
1 表示元素在 set 中。0 表示元素不在 set 中或者 key 不存在。
示例:
(4)SCARD
获取一个 set 的基数(cardinality),即 set 中的元素个数。
语法:
SCARD key
命令有效版本:
1.0.0 之后
时间复杂度:
O(1)
返回值:
set 内的元素个数。
示例:
(5)SPOP
从 set 中删除并返回⼀个或者多个元素。
注意:由于 set 内的元素是无序的,所以取出哪个元素实际是未定义行为,即可以看作随机的。
语法:
SPOP key [count]
命令有效版本:
1.0.0 之后
时间复杂度:
O(N),N 是 count
返回值:
取出的元素。
示例:
(6)SMOVE
将一个元素从源 set 取出并放入目标 set 中。
语法:
SMOVE source destination member
命令有效版本:
1.0.0 之后
时间复杂度:
O(1)
返回值:
1 表示移动成功,0 表示失败。
示例:
针对上述情况,smove 不会视为出错,也会按照删除、插入来执行。
(7)SREM
将指定的元素从 set 中删除。
语法:
SREM key member [member ...]
命令有效版本:
1.0.0 之后
时间复杂度:
O(N),N 是要删除的元素个数.
返回值:
本次操作删除的元素个数。
示例:
2、集合间操作
交集(inter)、并集(union)、差集(diff)的概念如下图所示:
集合求交集、并集、差集:
(1)SINTER
获取给定 set 的交集中的元素。
语法:
SINTER key [key ...]
命令有效版本:
1.0.0 之后
时间复杂度:
O(N * M),N 是最小的集合元素个数,M 是最大的集合元素个数。
返回值:
交集的元素。
示例:
(2)SINTERSTORE
获取给定 set 的交集中的元素并保存到目标 set 中。
要想知道交集的内容,直接按照集合的方式访问目标 set 这个 key 即可。
语法:
SINTERSTORE destination key [key ...]
命令有效版本:
1.0.0 之后
时间复杂度:
O(N * M),N 是最小的集合元素个数,M 是最大的集合元素个数。
返回值:
交集的元素个数。
示例:
(3)SUNION
获取给定 set 的并集中的元素。
语法:
SUNION key [key ...]
命令有效版本:
1.0.0 之后
时间复杂度:
O(N),N 给定的所有集合的总的元素个数。
返回值:
并集的元素。
示例:
(4)SUNIONSTORE
获取给定 set 的并集中的元素并保存到目标 set 中。
语法:
SUNIONSTORE destination key [key ...]
命令有效版本:
1.0.0 之后
时间复杂度:
O(N),N 给定的所有集合的总的元素个数。
返回值:
并集的元素个数。
示例:
(5)SDIFF
获取给定 set 的差集中的元素。
语法:
SDIFF key [key ...]
命令有效版本:
1.0.0 之后
时间复杂度:
O(N),N 给定的所有集合的总的元素个数。
返回值:
差集的元素。
示例:
(6)SDIFFSTORE
获取给定 set 的差集中的元素并保存到⽬标 set 中。
语法:
SDIFFSTORE destination key [key ...]
命令有效版本:
1.0.0 之后
时间复杂度:
O(N),N 给定的所有集合的总的元素个数.
返回值:
差集的元素个数。
示例:
3、命令小结
下表总结了集合类型的常见命令:
集合类型命令:
4、内部编码
集合类型的内部编码有两种:
- intset(整数集合):当集合中的元素都是整数并且元素的个数小于 set-max-intset-entries 配置(默认 512 个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使⽤。
- hashtable(哈希表):当集合类型无法满足 intset 的条件时,Redis 会使用 hashtable 作为集合的内部实现。
(1)当元素个数较少并且都为整数时,内部编码为 intset
(2)当元素个数超过 512 个,内部编码为 hashtable
(3)当存在元素不是整数时,内部编码为 hashtable
5、使用场景
场景一:集合类型比较典型的使用场景是标签(tag)。例如 A 用户对娱乐、体育板块比较感兴趣,B 用户对历史、新闻比较感兴趣,这些兴趣点可以被抽象为标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于增强用户体验和用户黏度都非常有帮助。 例如一个电子商务网站会对不同标签的用户做不同的产品推荐。
下面的演示通过集合类型来实现标签的若干功能。
(1)给用户添加标签
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4
(2)给标签添加用户
sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
...
sadd tagk:users user:1 user:4 user:9 user:28
(3)删除用户下的标签
srem user:1:tags tag1 tag5
...
(4)删除标签下的用户
srem tag1:users user:1
srem tag5:users user:1
...
(5)计算用户的共同兴趣标签
sinter user:1:tags user:2:tags
场景二:还可以使用 Set 来计算用户之间的共同好友(基于 “集合求交集”),基于此还可以做一些好友推荐。
场景三:
使用 Set 还能统计 UV(去重)。一个互联网产品如何衡量用户量,用户规模呢?
主要的指标是以下两个方面:
- PV(Page View),用户每次访问该服务器都会产生一个 pv。
- UV(User View),每个用户访问服务器都会产生一个 uv,但是同一个用户多次访问并不会使 uv 增加。uv 需要按照用户进行去重,去重的过程就可以使用 Set 来实现。
二、Zset 有序集合
有序集合相对于字符串、列表、哈希、集合来说会有一些陌生。它保留了集合不能有重复成员的特点,但与集合不同的是,有序集合中的每个元素都有一个唯一的浮点类型的分数(score)与之关联,着使得有序集合中的元素是可以维护有序性的,但这个有序不是用下标作为排序依据而是用这个分数。
Zset 的内部数据结构是跳表。
如下图所示,该有序集合显示了三国中的武将的武力。
有序集合:
有序集合提供了获取指定分数和元素范围查找、计算成员排名等功能,合理地利用有序集合,可以帮助我们在实际开发中解决很多问题。
有序集合中的元素是不能重复的,但分数允许重复。类比于一次考试之后,每个人一定有一个唯一的分数,但分数允许相同。
列表、集合、有序集合三者的异同点:
1、普通命令
(1)ZADD
添加或者更新指定的元素以及关联的分数到 zset 中,分数应该符合 double 类型,+inf/-inf 作为正负极限也是合法的。注意:负无穷大不是无穷小,负无穷大的绝对值和无穷大是一样的。
ZADD 的相关选项:
XX:仅仅用于更新已经存在的元素(member),不会添加新元素。
NX:仅用于添加新元素(member),不会更新已经存在的元素。
LT:仅当新分数小于当前分数时才更新现有元素,不会阻止添加新元素。
GT:仅当新分数大于当前分数时才更新现有元素,不会阻止添加新元素。
CH:默认情况下,ZADD 返回的是本次添加的元素个数,但指定这个选项之后,就会还包含本次更新的元素的个数。
INCR:此时命令类似 ZINCRBY 的效果,将元素的分数加上指定的分数。此时只能指定⼀个元素和分数。
语法:
ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]
member 和 score 称为是一个 "pair",类似于 C++ 里的 std::pair。不要把它们理解成 “键值对”(key - value pair),键值对中是有明确的 “角色区分”,一定是根据键 -> 值。而对于有序集合来说,既可以通过 member 找到对应的 score,也可以通过 score 找到匹配的 member。
命令有效版本:
1.2.0 之后
时间复杂度:
O(log(N))
返回值:
返回新增成功的元素个数。
示例:
如果修改的分数影响到了之前的顺序,就会自动的移动元素位置,保持原有的升序顺序不变:
(2)ZCARD
获取一个 zset 的基数(cardinality),即 zset 中的元素个数。
语法:
ZCARD key
命令有效版本:
1.2.0 之后
时间复杂度:
O(1)
返回值:
zset 内的元素个数。
示例:
(3)ZCOUNT
返回分数在 min 和 max 之间的元素个数,默认情况下,min 和 max 都是包含的,如果不想要边界值,可以通过在边界值前加上 '(' 来排除。
语法:
ZCOUNT key min max
命令有效版本:
2.0.0 之后
时间复杂度:
O(log(N))
先根据 min 找到对应的元素,再根据 max 找到对应的元素,两次都是 O(log(N))。实际上,Zset 内部会记录每个元素当前的 “排行” / “次序”,查询到元素就直接知道了元素所在的 “次序”(下标),就可以直接把 max 对应的元素次序和 min 对应的元素次序做减法即可。
返回值:
满足条件的元素列表个数。
示例:
(4)ZRANGE
返回指定区间里的元素,分数按照升序。带上 WITHSCORES 可以把分数也返回。
语法:
ZRANGE key start stop [WITHSCORES]
此处的 [start, stop] 为下标构成的区间,从 0 开始,支持负数。
命令有效版本:
1.2.0 之后
时间复杂度:
O(log(N)+M),N 是整个有序集合的元素个数,M 是 start - stop 区间内的元素个数。
返回值:
区间内的元素列表。
示例:
Redis 内部存储数据是按照二进制的方式存储的,意味着 Redis 服务器是不负责字符编码的,所以要把二进制对回到汉字需要客户端支持:
(5)ZREVRANGE
返回指定区间里的元素,分数按照降序。带上 WITHSCORES 可以把分数也返回。
备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。
语法:
ZREVRANGE key start stop [WITHSCORES]
命令有效版本:
1.2.0 之后
时间复杂度:
O(log(N)+M)
返回值:
区间内的元素列表。
示例:
(6)ZRANGEBYSCORE(弃用)
返回分数在 min 和 max 之间的元素,默认情况下,min 和 max 都是包含的,可以通过 '(' 排除。
备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。
语法:
ZRANGEBYSCORE key min max [WITHSCORES]
命令有效版本:
1.0.5 之后
时间复杂度:
O(log(N)+M)
返回值:
区间内的元素列表。
示例:
(7)ZPOPMAX
删除并返回分数最高的 count 个元素。
语法:
ZPOPMAX key [count]
命令有效版本:
5.0.0 之后
时间复杂度:
O(log(N) * M),N 是有序集合的元素个数,M 表示 count,要删除的元素个数。
既然是尾删,为什么不把最后一个元素的位置特殊标记一下,后续删除不久省却了查找过程,直接 O(1) 了吗?
这个是有可能的,但是目前 Redis 并没有这么做。事实上,Redis 的源码中针对有序集合确实是记录了尾部的特定位置,但是在实际删除的时候并没有用上这个特性,而是直接调用了一个 “通用的删除函数”(给定一个 member 的值,进行查找,找到位置之后再删除)。(个人认为此处是存在优化空间的)
返回值:
分数和元素列表。
示例:
如果存在多个元素分数相同(分数是主要因素,相同的情况下会按照 member 字符串的字典序来决定先后顺序),同时为最大值,那么 zpopmax 删除最大元素时,仍然只会删除其中一个元素。
(8)BZPOPMAX
ZPOPMAX 的阻塞版本。可以同时读多个有序集合。
语法:
BZPOPMAX key [key ...] timeout
timeout 单位是 s,支持小数形式。
命令有效版本:
5.0.0 之后
时间复杂度:
O(log(N)),删除最大值花费的时间。
如果当前 BZPOPMAX 同时监听多个 key,假设 key 是 M 个,那么此时时间复杂度是 O(log(N) * M) 吗?
每个这样的 key 上面都删除一次元素才需要 * M,而这里是从这若干个 key 中只删除一次。
返回值:
元素列表。
示例:
(9)ZPOPMIN
删除并返回分数最低的 count 个元素。
语法:
ZPOPMIN key [count]
命令有效版本:
5.0.0 之后
时间复杂度:
O(log(N) * M)
返回值:
分数和元素列表。
示例:
(10)BZPOPMIN
ZPOPMIN 的阻塞版本。
语法:
BZPOPMIN key [key ...] timeout
命令有效版本:
5.0.0 之后
时间复杂度:
O(log(N))
返回值:
元素列表。
示例:
(11)ZRANK
返回指定元素的排名,升序。
语法:
ZRANK key member
命令有效版本:
2.0.0 之后
时间复杂度:
O(log(N))
ZRANK 查找元素的过程和 ZCOUNT 是一样的。
返回值:
排名。
示例:
(12)ZREVRANK
返回指定元素的排名,降序。
语法:
ZREVRANK key member
命令有效版本:
2.0.0 之后
时间复杂度:
O(log(N))
返回值:
排名。
示例:
(13)ZSCORE
返回指定元素的分数。
语法:
ZSCORE key member
命令有效版本:
1.2.0 之后
时间复杂度:
O(1)
此处相当于 Redis 对于这样的查询操作做了特殊优化,付出了额外的空间代价。
返回值:
分数。
示例:
(14)ZREM
删除指定的元素。
语法:
ZREM key member [member ...]
命令有效版本:
1.2.0 之后
时间复杂度:
O(M*log(N))
返回值:
本次操作删除的元素个数。
示例:
(15)ZREMRANGEBYRANK
按照排序,升序删除指定范围的元素,左闭右闭。
语法:
ZREMRANGEBYRANK key start stop
命令有效版本:
2.0.0 之后
时间复杂度:
O(log(N)+M)
返回值:
本次操作删除的元素个数。
示例:
(16)ZREMRANGEBYSCORE
按照分数删除指定范围的元素,左闭右闭,也可以使用 '(' 来排除边界值。
语法:
ZREMRANGEBYSCORE key min max
命令有效版本:
1.2.0 之后
时间复杂度:
O(log(N)+M)
返回值:
本次操作删除的元素个数。
示例:
(17) ZINCRBY
为指定的元素的关联分数添加指定的分数值。
语法:
ZINCRBY key increment member
命令有效版本:
1.2.0 之后
时间复杂度:
O(log(N))
返回值:
增加后元素的分数。
示例:
ZINCRBY 不光会修改分数内容,也能同时移动元素位置,保证整个有序集合仍然是升序的。
2、集合间操作
有序集合的交集操作:
(1)ZINTERSTORE
求出给定有序集合中元素的交集并保存进目标有序集合中,在合并过程中以元素为单位进行合并,元素对应的分数按照不同的聚合方式和权重得到新的分数。
在有序集合中,member 是元素的本体,score 只是辅助排序的工具人。因此,在进行比较 “相同” 时,只要 member 相同即可。如果 member 相同,score 不同,进行交集合并之后的最终分数看 AGGREGATE 后面的属性。
语法:
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>]numkeys 是一个整数,用来描述后续有几个 key 参与交集运算。
命令有效版本:
2.0.0 之后
时间复杂度:
O(N*K)+O(M*log(M)),N 是输入的有序集合中,最小的有序集合的元素个数;K 是输入了几个有序集合;M 是最终结果的有序集合的元素个数。
返回值:
目标集合中的元素个数。
示例:
有序集合的并集操作:
(2)ZUNIONSTORE
求出给定有序集合中元素的并集并保存进目标有序集合中,在合并过程中以元素为单位进行合并,元素对应的分数按照不同的聚合方式和权重得到新的分数。
语法:
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>]
命令有效版本:
2.0.0 之后
时间复杂度:
O(N)+O(M*log(M)) N 是输入的有序集合总的元素个数,M 是最终结果的有序集合的元素个数。
返回值:
目标集合中的元素个数
示例:
3、命令小结
有序集合命令:
4、内部编码
有序集合类型的内部编码有两种:
- ziplist(压缩列表):当有序集合的元素个数小于 zset-max-ziplist-entries 配置(默认 128 个),同时每个元素的值都小于 zset-max-ziplist-value 配置(默认 64 字节)时,Redis 会用 ziplist 来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
- skiplist(跳表):当 ziplist 条件不满足时,有序集合会使用 skiplist 作为内部实现,因为此时 ziplist 的操作效率会下降。
简单来说,跳表是一个 “复杂链表”,查询元素的时间复杂度是 O(logN)。相比于树形结构,更适合按照范围获取元素。
(1)当元素个数较少且每个元素较小时,内部编码为 ziplist
(2)当元素个数超过 128 个,内部编码 skiplist
(3)当某个元素大于 64 字节时,内部编码 skiplist
5、使用场景
有序集合比较典型的使用场景就是排行榜系统。例如常见的网站上的热榜信息,榜单的维度可能是多方面的:按照时间、按照阅读量、按照点赞量。本例中我们使用点赞数这个维度,维护每天的热榜:
(1)添加用户赞数
例如用户 james 发布了一篇文章,并获得 3 个赞,可以使用有序集合的 zadd 和 zincrby 功能:
zadd user:ranking:2022-03-15 3 james
之后如果再获得赞,可以使用 zincrby:
zincrby user:ranking:2022-03-15 1 james
(2)取消用户赞数
由于各种原因(例如用户注销、用户作弊等)需要将用户删除,此时需要将用户从榜单中删除掉,可以使用 zrem。例如删除成员 tom:
zrem user:ranking:2022-03-15 tom
(3)展示获取赞数最多的 10 个用户
此功能使用 zrevrange 命令实现:
zrevrangebyrank user:ranking:2022-03-15 0 9
(4)展示用户信息以及用户分数
次功能将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户的分数和排名可以使用 zscore 和 zrank 来实现。
hgetall user:info:tom
zscore user:ranking:2022-03-15 mike
zrank user:ranking:2022-03-15 mike
三、渐进式遍历
Redis 使用 scan 命令进行渐进式遍历键,进而解决直接使用 keys 获取键时可能出现的阻塞问题。不是一个命令将所有的 key 都拿到,而是每执行一次命令,只获取到其中的一小部分,这样就可以保证当前这一次操作不会太卡。每次 scan 命令的时间复杂度是 O(1),但是要完整地完成所有键的遍历,需要执行多次 scan。渐进式遍历其实是一组命令,这一组命令的使用方法是一样的。整个过程如下图所示:
scan 命令渐进式遍历:
- 首次 scan 从 0 开始。
- 当 scan 返回的下次位置为 0 时,遍历结束。
返回值的前半部分1)是告诉我们,下次继续遍历的光标(当作一个字符串即可)要从哪里开始。第二部分2)是我们真正遍历到的 key 的内容。
1、SCAN
以渐进式的方式进行键的遍历。
渐进式遍历再遍历过程中不会在服务器这边存储任何的状态信息,此处的遍历是随时可以终止的,不会对服务器产生任何的副作用。
语法:
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
这里的 count 是限制这一次遍历能够获取到多少个元素,默认是 10。
注意:此处的 count(给 Redis 服务器一个 “提示” / “建议”,写入的 count 和实际返回的 key 的个数不一定完全相同,但是不会差很多)和 MySQL 的 limit(精确的)不一样。
命令有效版本:
2.8.0 之后
时间复杂度:
O(1)
返回值:
下一次 scan 的游标(cursor)以及本次得到的键。
示例:
除了 scan 以外,Redis 面向哈希类型、集合类型、有序集合类型分别提供了 hscan、sscan、zscan 命令,它们的用法和 scan 基本类似。
注意:渐进性遍历 scan 虽然解决了阻塞的问题,但如果在遍历期间键有所变化(增加、修改、删除),可能导致遍历时键的重复遍历或者遗漏。
四、数据库管理
Redis 提供了几个面向 Redis 数据库的操作,分别是命令:dbsize、select、flushdb、flushall。
1、切换数据库
select dbIndex
许多关系型数据库,例如 MySQL 支持在一个实例下有多个数据库存在的,但是与关系型数据库用字符来区分不同数据库名不同,Redis 只是用数字作为多个数据库的实现。Redis 默认配置中是有 16 个数据库,我们不能创建新的数据库,也不能删除已有的数据库,这 16 个数据库中的数据是隔离的(相互之间不会有影响)。select 0 操作会切换到第⼀个数据库,select 15 会切换到最后⼀个数据库。
0 号数据库和 15 号数据库保存的数据是完全不冲突的,如下图所示,即各种有各自的键值对。默认情况下,我们处于数据库 0。
Redis 管理的数据库:
Redis 中虽然支持多数据库,但随着版本的升级,其实不是特别建议使用多数据库特性。如果真的需要完全隔离的两套键值对,更好的做法是维护多个 Redis 实例,而不是在一个 Redis 实例中维护多数据库。这是因为本身 Redis 并没有为多数据库提供太多的特性,其次无论是否有多个数据库,Redis 都是使用单线程模型,所以彼此之间还是需要排队等待命令的执行。同时多数据库还会让开发、调试和运维工作变得复杂。所以实践中,始终使用数据库 0 其实是⼀个很好的选择
2、清除数据库
flushdb / flushall 命令用于清除数据库,区别在于 flushdb 只清除当前数据库,flushall 会清楚所有数据库。
永远不要在线上环境执行清除数据的操作,除非你想体验一把 “从删库到跑路” 的操作。
语法:
FLUSHDB [ASYNC | SYNC]
FLUSHALL [ASYNC | SYNC]
- ASYNC:异步
- SYNC:同步