这是我参与更文挑战的第23天,活动详情查看:更文挑战
有序集合,从名字上看,可知道包含两个特性:有序、元素不重复。有序集合通过个每个元素设置一个分数(score),依据分数顺序实现元素有序(如下图)。虽然有序集合的元素不能重复,但是score
可以重复。
有序集合的命令:
ZADD key score1 member1 [score2 member2] //向有序集合添加一个或多个成员,或者更新已存在成员的分数
ZCARD key //获取有序集合的成员数
ZCOUNT key min max //计算在有序集合中指定区间分数的成员数
ZINCRBY key increment member //有序集合中对指定成员的分数加上增量 increment
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] //迭代有序集合中的元素(包括元素成员和元素分值)
有序集合编码
有序集合的编码可以是ziplist
或者skiplist
。
ziplist 编码的有序集合
ziplist
编码的有序集合对象使用压缩列表作为底层实现, 每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员(member), 而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序, 分值较小的元素被放置在靠近表头的方向, 而分值较大的元素则被放置在靠近表尾的方向。
skiplist 编码的有序集合
skiplist
编码的有序集合对象使用 zset
结构作为底层实现, 一个 zset
结构同时包含一个字典和一个跳跃表:
typedef struct zset {
zskiplist *zsl;
dict *dict;
} zset;
zset
结构中的 zsl
跳跃表按分值从小到大保存了所有集合元素, 每个跳跃表节点都保存了一个集合元素: 跳跃表节点的 object
属性保存了元素的成员, 而跳跃表节点的 score
属性则保存了元素的分值。 通过这个跳跃表, 程序可以对有序集合进行范围型操作, 比如 ZRANK 、 ZRANGE 等命令就是基于跳跃表 API 来实现的。
除此之外, zset
结构中的 dict
字典为有序集合创建了一个从成员到分值的映射, 字典中的每个键值对都保存了一个集合元素: 字典的键保存了元素的成员, 而字典的值则保存了元素的分值。 通过这个字典, 程序可以用 O(1) 复杂度查找给定成员的分值, ZSCORE 命令就是根据这一特性实现的, 而很多其他有序集合命令都在实现的内部用到了这一特性。
有序集合每个元素的成员都是一个字符串对象, 而每个元素的分值都是一个 double
类型的浮点数。 值得一提的是, 虽然 zset
结构同时使用跳跃表和字典来保存有序集合元素, 但这两种数据结构都会通过指针来共享相同元素的成员和分值, 所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值, 也不会因此而浪费额外的内存。
为什么有序集合需要同时使用跳跃表和字典来实现?
- 在理论上来说, 有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现, 但无论单独使用字典还是跳跃表, 在性能上对比起同时使用字典和跳跃表都会有所降低。
- 举个例子, 如果我们只使用字典来实现有序集合, 那么虽然以 O(1) 复杂度查找成员的分值这一特性会被保留, 但是, 因为字典以无序的方式来保存集合元素, 所以每次在执行范围型操作 —— 比如 ZRANK 、 ZRANGE 等命令时, 程序都需要对字典保存的所有元素进行排序, 完成这种排序需要至少 O(N \log N) 时间复杂度, 以及额外的 O(N) 内存空间 (因为要创建一个数组来保存排序后的元素)。
- 另一方面, 如果我们只使用跳跃表来实现有序集合, 那么跳跃表执行范围型操作的所有优点都会被保留, 但因为没有了字典, 所以根据成员查找分值这一操作的复杂度将从 O(1) 上升为 O(\log N) 。
- 因为以上原因, 为了让有序集合的查找和范围型操作都尽可能快地执行, Redis 选择了同时使用字典和跳跃表两种数据结构来实现有序集合。
编码转换
当有序集合对象可以同时满足以下两个条件时, 对象使用 ziplist
编码:
-
有序集合保存的元素数量小于
128
个; -
有序集合保存的所有元素成员的长度都小于
64
字节;
不能满足以上两个条件的有序集合对象将使用 skiplist
编码。
注意
以上两个条件的上限值是可以修改的, 具体请看配置文件中关于
zset-max-ziplist-entries
选项和zset-max-ziplist-value
选项的说明。