有序集合对象¶
有序集合的编码可以是 ziplist
或者 skiplist
。
ziplist
编码的有序集合对象使用压缩列表作为底层实现,
每个集合元素使用两个紧挨在一起的压缩列表节点来保存,
第一个节点保存元素的成员(member),
而第二个元素则保存元素的分值(score)。
压缩列表内的集合元素按分值从小到大进行排序, 分值较小的元素被放置在靠近表头的方向, 而分值较大的元素则被放置在靠近表尾的方向。
举个例子,
如果我们执行以下 ZADD 命令,
那么服务器将创建一个有序集合对象作为 price
键的值:
redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
如果 price
键的值对象使用的是 ziplist
编码,
那么这个值对象将会是图 8-14 所示的样子,
而对象所使用的压缩列表则会是 8-15 所示的样子。
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
结构同时使用跳跃表和字典来保存有序集合元素,
但这两种数据结构都会通过指针来共享相同元素的成员和分值,
所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值,
也不会因此而浪费额外的内存。
举个例子,
如果前面 price
键创建的不是 ziplist
编码的有序集合对象,
而是 skiplist
编码的有序集合对象,
那么这个有序集合对象将会是图 8-16 所示的样子,
而对象所使用的 zset
结构将会是图 8-17 所示的样子。
编码的转换¶
当有序集合对象可以同时满足以下两个条件时,
对象使用 ziplist
编码:
有序集合保存的元素数量小于
128
个;有序集合保存的所有元素成员的长度都小于
64
字节;
不能满足以上两个条件的有序集合对象将使用 skiplist
编码。
对于使用 ziplist
编码的有序集合对象来说,
当使用 ziplist
编码所需的两个条件中的任意一个不能被满足时,
程序就会执行编码转换操作,
将原本储存在压缩列表里面的所有集合元素转移到 zset
结构里面,
并将对象的编码从 ziplist
改为 skiplist
。
以下代码展示了有序集合对象因为包含了过多元素而引发编码转换的情况:
# 对象包含了 128 个元素
redis> EVAL "for i=1, 128 do redis.call('ZADD', KEYS[1], i, i) end" 1 numbers
(nil)
redis> ZCARD numbers
(integer) 128
redis> OBJECT ENCODING numbers
"ziplist"
# 再添加一个新元素
redis> ZADD numbers 3.14 pi
(integer) 1
# 对象包含的元素数量变为 129 个
redis> ZCARD numbers
(integer) 129
# 编码已改变
redis> OBJECT ENCODING numbers
"skiplist"
以下代码则展示了有序集合对象因为元素的成员过长而引发编码转换的情况:
# 向有序集合添加一个成员只有三字节长的元素
redis> ZADD blah 1.0 www
(integer) 1
redis> OBJECT ENCODING blah
"ziplist"
# 向有序集合添加一个成员为 66 字节长的元素
redis> ZADD blah 2.0 oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
(integer) 1
# 编码已改变
redis> OBJECT ENCODING blah
"skiplist"
有序集合命令的实现¶
因为有序集合键的值为有序集合对象, 所以用于有序集合键的所有命令都是针对有序集合对象来构建的, 表 8-11 列出了其中一部分有序集合键命令, 以及这些命令在不同编码的有序集合对象下的实现方法。
表 8-11 有序集合命令的实现方法
命令 |
|
|
---|---|---|
ZADD |
调用 |
先调用 |
ZCARD |
调用 |
访问跳跃表数据结构的 |
ZCOUNT |
遍历压缩列表, 统计分值在给定范围内的节点的数量。 |
遍历跳跃表, 统计分值在给定范围内的节点的数量。 |
ZRANGE |
从表头向表尾遍历压缩列表, 返回给定索引范围内的所有元素。 |
从表头向表尾遍历跳跃表, 返回给定索引范围内的所有元素。 |
ZREVRANGE |
从表尾向表头遍历压缩列表, 返回给定索引范围内的所有元素。 |
从表尾向表头遍历跳跃表, 返回给定索引范围内的所有元素。 |
ZRANK |
从表头向表尾遍历压缩列表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 |
从表头向表尾遍历跳跃表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 |
ZREVRANK |
从表尾向表头遍历压缩列表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 |
从表尾向表头遍历跳跃表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 |
ZREM |
遍历压缩列表, 删除所有包含给定成员的节点, 以及被删除成员节点旁边的分值节点。 |
遍历跳跃表, 删除所有包含了给定成员的跳跃表节点。 并在字典中解除被删除元素的成员和分值的关联。 |
ZSCORE |
遍历压缩列表, 查找包含了给定成员的节点, 然后取出成员节点旁边的分值节点保存的元素分值。 |
直接从字典中取出给定成员的分值。 |