文档:有序集合

有序集合是唯一字符串(成员)的集合,这些字符串通过关联的分数进行排序。当多个字符串具有相同分数时,它们会按字典顺序排序。有序集合的一些用例包括

  • 排行榜。例如,您可以使用有序集合轻松维护大型在线游戏中的最高分数排序列表。
  • 速率限制器。特别是,您可以使用有序集合构建滑动窗口速率限制器,以防止过多的 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 实例都将返回相同的输出。

用于操作字典序范围的主要命令是 ZRANGEBYLEXZREVRANGEBYLEXZREMRANGEBYLEXZLEXCOUNT

例如,让我们再次添加我们的著名黑客列表,但这次为所有元素使用零分。我们将看到,由于有序集合的排序规则,它们已经按字典顺序排序。使用 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 是返回结果的数量。