有序集合是唯一字符串(成员)的集合,这些字符串通过关联的分数进行排序。当多个字符串具有相同分数时,它们会按字典顺序排序。有序集合的一些用例包括
- 排行榜。例如,您可以使用有序集合轻松维护大型在线游戏中的最高分数排序列表。
- 速率限制器。特别是,您可以使用有序集合构建滑动窗口速率限制器,以防止过多的 API 请求。
您可以将有序集合视为集合(Set)和哈希(Hash)的混合体。与集合类似,有序集合由唯一、不重复的字符串元素组成,因此在某种意义上,有序集合也是一种集合。
然而,集合内部的元素是无序的,而有序集合中的每个元素都关联着一个浮点数值,称为分数(这就是为什么该类型也类似于哈希,因为每个元素都映射到一个值)。
此外,有序集合中的元素是按顺序获取的(因此它们不是按需排序的,顺序是有序集合所用数据结构的一个特点)。它们根据以下规则进行排序:
- 如果 B 和 A 是两个分数不同的元素,那么如果 A 的分数 > B 的分数,则 A > B。
- 如果 B 和 A 的分数完全相同,那么如果字符串 A 在字典顺序上大于字符串 B,则 A > B。B 和 A 的字符串不能相等,因为有序集合只包含唯一元素。
让我们从一个简单的例子开始,我们将添加所有赛车手以及他们在第一场比赛中获得的分数
127.0.0.1:6379> ZADD racer_scores 10 "Norem"
(integer) 1
127.0.0.1:6379> ZADD racer_scores 12 "Castilla"
(integer) 1
127.0.0.1:6379> ZADD racer_scores 8 "Sam-Bodden" 10 "Royce" 6 "Ford" 14 "Prickett"
(integer) 4
如您所见,ZADD
类似于 SADD
,但它接受一个额外的参数(放在要添加的元素之前),即分数。ZADD
也是可变参数的,因此您可以自由指定多个分数-值对,即使上述示例中没有使用。
使用有序集合,按出生年份排序的黑客列表很容易返回,因为实际上它们已经排序好了。
实现说明:有序集合是通过包含跳跃表和哈希表的双端口数据结构实现的,因此每次添加元素时,Valkey 都会执行 O(log(N)) 操作。这很好,但是当我们请求排序元素时,Valkey 完全不需要做任何额外工作,它已经排好序了。请注意,ZRANGE
命令的顺序是从低到高,而 ZREVRANGE
命令的顺序是从高到低
127.0.0.1:6379> ZRANGE racer_scores 0 -1
1) "Ford"
2) "Sam-Bodden"
3) "Norem"
4) "Royce"
5) "Castilla"
6) "Prickett"
127.0.0.1:6379> ZREVRANGE racer_scores 0 -1
1) "Prickett"
2) "Castilla"
3) "Royce"
4) "Norem"
5) "Sam-Bodden"
6) "Ford"
注意:0 和 -1 表示从索引 0 到最后一个元素(-1 在这里的作用与 LRANGE
命令中一样)。
也可以使用 WITHSCORES
参数返回分数。
127.0.0.1:6379> ZRANGE racer_scores 0 -1 withscores
1) "Ford"
2) "6"
3) "Sam-Bodden"
4) "8"
5) "Norem"
6) "10"
7) "Royce"
8) "10"
9) "Castilla"
10) "12"
11) "Prickett"
12) "14"
对范围进行操作
有序集合比这更强大。它们可以对范围进行操作。让我们获取所有分数等于或低于 10 分的赛车手。我们使用 ZRANGEBYSCORE
命令来完成此操作
127.0.0.1:6379> ZRANGEBYSCORE racer_scores -inf 10
1) "Ford"
2) "Sam-Bodden"
3) "Norem"
4) "Royce"
我们要求 Valkey 返回所有分数介于负无穷大和 10 之间的元素(包括两端)。
要删除一个元素,我们只需使用赛车手的名字调用 ZREM
。也可以删除一系列元素。让我们删除赛车手 Castilla 以及所有分数严格低于 10 分的赛车手
127.0.0.1:6379> ZREM racer_scores "Castilla"
(integer) 1
127.0.0.1:6379> ZREMRANGEBYSCORE racer_scores -inf 9
(integer) 2
127.0.0.1:6379> ZRANGE racer_scores 0 -1
1) "Norem"
2) "Royce"
3) "Prickett"
ZREMRANGEBYSCORE
可能不是最好的命令名称,但它非常有用,并返回已删除元素的数量。
为有序集合元素定义的另一个极其有用的操作是获取排名的操作。可以查询一个元素在有序元素集合中的位置。ZREVRANK
命令也可用,用于获取元素按降序排序时的排名。
127.0.0.1:6379> ZRANK racer_scores "Norem"
(integer) 0
127.0.0.1:6379> ZREVRANK racer_scores "Norem"
(integer) 3
字典序分数
一系列命令允许按字典顺序获取范围,前提是所有有序集合中的元素都插入了相同的分数。元素使用 C 语言的 memcmp
函数进行比较,因此保证没有排序规则,并且每个 Valkey 实例都将返回相同的输出。
用于操作字典序范围的主要命令是 ZRANGEBYLEX
、ZREVRANGEBYLEX
、ZREMRANGEBYLEX
和 ZLEXCOUNT
。
例如,让我们再次添加我们的著名黑客列表,但这次为所有元素使用零分。我们将看到,由于有序集合的排序规则,它们已经按字典顺序排序。使用 ZRANGEBYLEX
我们可以请求字典序范围
127.0.0.1:6379> ZADD racer_scores 0 "Norem" 0 "Sam-Bodden" 0 "Royce" 0 "Castilla" 0 "Prickett" 0 "Ford"
(integer) 3
127.0.0.1:6379> ZRANGE racer_scores 0 -1
1) "Castilla"
2) "Ford"
3) "Norem"
4) "Prickett"
5) "Royce"
6) "Sam-Bodden"
127.0.0.1:6379> ZRANGEBYLEX racer_scores [A [L
1) "Castilla"
2) "Ford"
范围可以是包含的或不包含的(取决于第一个字符),字符串无穷大和负无穷大分别用 +
和 -
字符串指定。有关更多信息,请参阅文档。
此功能很重要,因为它允许我们将有序集合用作通用索引。例如,如果您想按 128 位无符号整数参数索引元素,您所需要做的就是将元素添加到具有相同分数(例如 0)但带有 16 字节前缀的有序集合中,该前缀由大端序的 128 位数字组成。由于大端序的数字在按字典顺序(原始字节顺序)排序时实际上也是按数字顺序排序的,因此您可以在 128 位空间中请求范围,并获取元素的实际值(丢弃前缀)。
更新分数:排行榜
在切换到下一个主题之前,关于有序集合的最后一点说明。有序集合的分数可以随时更新。只需对有序集合中已包含的元素调用 ZADD
即可更新其分数(和位置),时间复杂度为 O(log(N))。因此,当有大量更新时,有序集合是合适的。
由于此特性,一个常见的用例是排行榜。典型的应用是 Facebook 游戏,您结合了按高分排序用户以及获取排名操作的能力,以便显示前 N 名用户以及用户在排行榜中的排名(例如,“您是这里的第 #4932 名最佳分数”)。
示例
- 有两种方式可以使用有序集合表示排行榜。如果我们知道赛车手的新分数,我们可以直接通过
ZADD
命令更新它。然而,如果我们要在一个现有分数上添加分数,我们可以使用ZINCRBY
命令。
127.0.0.1:6379> ZADD racer_scores 100 "Wood"
(integer) 1
127.0.0.1:6379> ZADD racer_scores 100 "Henshaw"
(integer) 1
127.0.0.1:6379> ZADD racer_scores 150 "Henshaw"
(integer) 0
127.0.0.1:6379> ZINCRBY racer_scores 50 "Wood"
"150"
127.0.0.1:6379> ZINCRBY racer_scores 50 "Henshaw"
"200"
您会看到,当成员已存在时,ZADD
返回 0(分数被更新),而 ZINCRBY
返回新分数。赛车手 Henshaw 的分数从 100 变为 150(不考虑之前分数),然后又增加了 50 分达到 200。
基本命令
ZADD
将新成员及其关联分数添加到有序集合中。如果成员已存在,则更新分数。ZRANGE
返回有序集合的成员,在给定范围内排序。ZRANK
返回给定成员的排名,假设有序集合按升序排列。ZREVRANK
返回给定成员的排名,假设有序集合按降序排列。
查看完整的有序集合命令列表。
性能
大多数有序集合操作的时间复杂度为 O(log(n)),其中 n 是成员的数量。
在使用 ZRANGE
命令返回大量结果(例如,数万个或更多)时,需要谨慎。此命令的时间复杂度为 O(log(n) + m),其中 m 是返回结果的数量。