目录
1. Zset 有序集合
有序集合相对于字符串、列表、哈希、集合来说会有一些陌生。它保留了集合不能有重复成员的特点,但与集合不同的是,有序集合中的每个元素都有一个唯一的浮点类型的分数(score)与之关联,着使得有序集合中的元素是可以维护有序性的,但这个有序不是用下标作为排序依据而是用这个分数。
Zset 的内部数据结构是跳表。
如下图所示,该有序集合显示了三国中的武将的武力。
有序集合:
有序集合提供了获取指定分数和元素范围查找、计算成员排名等功能,合理地利用有序集合,可以帮助我们在实际开发中解决很多问题。
有序集合中的元素是不能重复的,但分数允许重复。类比于一次考试之后,每个人一定有一个唯一的分数,但分数允许相同。
列表、集合、有序集合三者的异同点:
1.1 Zset 有序集合常见命令
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))
返回值:返回新增成功的元素个数。
示例:
如果修改的分数影响到了之前的顺序,就会自动的移动元素位置,保持原有的升序顺序不变:
不存在才插入:
存在才插入:
返回更新元素的个数:
给元素加上指定分数:
zcard
获取一个 zset 的基数(cardinality),即 zset 中的元素个数。
语法:zcard key
命令有效版本:1.2.0 之后
时间复杂度:O(1)
返回值:zset 内的元素个数。
示例:
zcount
返回分数在 min 和 max 之间的元素个数,默认情况下,min 和 max 都是包含的,如果不想要边界值,可以通过在边界值前加上 '(' 来排除。
语法:zcount key min max
命令有效版本:2.0.0 之后
时间复杂度:O(log(N))。先根据 min 找到对应的元素,再根据 max 找到对应的元素,两次都是 O(log(N))。实际上,Zset 内部会记录每个元素当前的 “排行” / “次序”,查询到元素就直接知道了元素所在的 “次序”(下标),就可以直接把 max 对应的元素次序和 min 对应的元素次序做减法即可。
返回值:满足条件的元素列表个数。
示例:(注意最后括号的写法,与常见的不同)
zrange
返回指定区间里的元素,分数按照升序。带上withscores可以把分数也返回。
语法:zrange key start stop [withscores] 。此处的 [start, stop] 为下标构成的区间,从 0 开始,支持负数。
命令有效版本:1.2.0 之后
时间复杂度:O(log(N)+M),N 是整个有序集合的元素个数,M 是 start - stop 区间内的元素个数。
返回值:区间内的元素列表。
示例:
Redis 内部存储数据是按照二进制的方式存储的,意味着 Redis 服务器是不负责字符编码的,所以要把二进制对回到汉字需要客户端支持:
zrevrange
返回指定区间里的元素,分数按照降序。带上 withscores可以把分数也返回。
备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。
语法:zrevrangekey start stop [withscores]
命令有效版本:1.2.0 之后
时间复杂度:O(log(N)+M)
返回值:区间内的元素列表。
示例:
zrangebyscore(弃用)
返回分数在 min 和 max 之间的元素,默认情况下,min 和 max 都是包含的,可以通过 '(' 排除。
备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。
语法:zrangebyscore key min max [withscore]
命令有效版本:1.0.5 之后
时间复杂度:O(log(N)+M)
返回值:区间内的元素列表。
示例:
zpopmax
删除并返回分数最高的 count 个元素。
语法:zpopmax key [count]
命令有效版本:5.0.0 之后
时间复杂度:O(log(N) * M),N 是有序集合的元素个数,M 表示 count,要删除的元素个数。
既然是尾删,为什么不把最后一个元素的位置特殊标记一下,后续删除不久省却了查找过程,直接 O(1) 了吗?
这个是有可能的,但是目前 Redis 并没有这么做。事实上,Redis 的源码中针对有序集合确实是记录了尾部的特定位置,但是在实际删除的时候并没有用上这个特性,而是直接调用了一个 “通用的删除函数”(给定一个 member 的值,进行查找,找到位置之后再删除)。(一些人认为此处是存在优化空间的)
返回值:分数和元素列表。
示例:
如果存在多个元素分数相同(分数是主要因素,相同的情况下会按照 member 字符串的字典序来决定先后顺序),同时为最大值,那么 zpopmax 删除最大元素时,仍然只会删除其中一个元素。
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 中只删除一次。
返回值:元素列表。
示例:
zpopmin
删除并返回分数最低的 count 个元素。
语法:zpopmin key [count]
命令有效版本:5.0.0 之后
时间复杂度:O(log(N) * M)
返回值:分数和元素列表。
示例:
bzpopmin
zpopmin 的阻塞版本。
语法:bzpopmin key [key ...] timeout
命令有效版本:5.0.0 之后
时间复杂度:O(log(N))
返回值:元素列表。
和bzpopmax阻塞效果类似,不做演示。
zrank
返回指定元素的排名,升序。
语法:zrank key member
命令有效版本:2.0.0 之后
时间复杂度:O(log(N))。zrank 查找元素的过程和 zcount是一样的。
返回值:排名。
示例:
zrevrank
返回指定元素的排名,降序。
语法:zrevrank key member
命令有效版本:2.0.0 之后
时间复杂度:O(log(N))
返回值:排名。
示例:
zscore
返回指定元素的分数。
语法:zscore key member
命令有效版本:1.2.0 之后
时间复杂度:O(1)。此处相当于 Redis 对于这样的查询操作做了特殊优化,付出了额外的空间代价。
返回值:分数。
示例:
zrem
删除指定的元素。
语法:zrem key member [member ...]
命令有效版本:1.2.0 之后
时间复杂度:O(M*log(N))
返回值:本次操作删除的元素个数。
示例:
zremrangebyrank
按照排序,升序删除指定范围的元素,左闭右闭。
语法:zremrangebyrank key start stop
命令有效版本:2.0.0 之后
时间复杂度:O(log(N)+M)
返回值:本次操作删除的元素个数。
示例:
zremrangebyscore
按照分数删除指定范围的元素,左闭右闭,也可以使用 '(' 来排除边界值。
语法:zremrangebyscore key min max
命令有效版本:1.2.0 之后
时间复杂度:O(log(N)+M)
返回值:本次操作删除的元素个数。
示例:
zincrby
为指定的元素的关联分数添加指定的分数值。
语法:zincrby key increment member
命令有效版本:1.2.0 之后
时间复杂度:O(log(N))
返回值:增加后元素的分数。
示例:
zincrby 不光会修改分数内容,也能同时移动元素位置,保证整个有序集合仍然是升序的。
1.2 Zset有序集合间操作
有序集合的交集操作:
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 是最终结果的有序集合的元素个数。
返回值:目标集合中的元素个数。
示例:
zunionstore
求出给定有序集合中元素的并集并保存进目标有序集合中,在合并过程中以元素为单位进行合并,元素对应的分数按照不同的聚合方式和权重得到新的分数。
有序集合的并集操作:
语法:zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate <sum| min | max>]
命令有效版本:2.0.0 之后
时间复杂度:O(N)+O(M*log(M)) N 是输入的有序集合总的元素个数,M 是最终结果的有序集合的元素个数。
返回值:目标集合中的元素个数
示例:
1.3 Zset命令小结和内部编码
有序集合命令:
Zset有序集合类型的内部编码有两种:
- 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
1.4 Zset有序集合使用场景
有序集合比较典型的使用场景就是排行榜系统。例如常见的网站上的热榜信息,榜单的维度可能是多方面的:按照时间、按照阅读量、按照点赞量。本例中我们使用点赞数这个维度,维护每天的热榜:
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存储⑦其它数据类型+渐进式遍历+数据库管理