SCAN

用法
SCAN cursor [ MATCH pattern ] [ COUNT count ] [ TYPE type ]
复杂度
每次调用 O(1)。完整迭代 O(N),包括足够多次的命令调用,直到游标返回 0。N 是集合中的元素数量。
始于
2.8.0
ACL 类别
@keyspace, @read, @slow

SCAN 命令以及密切相关的 SSCANHSCANZSCAN 命令用于增量迭代元素集合。

  • SCAN 迭代当前所选 Valkey 数据库中的键集合。
  • SSCAN 迭代 Set 类型中的元素。
  • HSCAN 迭代 Hash 类型中的字段及其关联值。
  • ZSCAN 迭代 Sorted Set 类型中的元素及其关联分数。

由于这些命令支持增量迭代,每次调用只返回少量元素,因此它们可以在生产环境中使用,而不会像 KEYSSMEMBERS 等命令那样,在处理大型键或元素集合时可能长时间阻塞服务器(甚至数秒)。

然而,尽管像 SMEMBERS 这样的阻塞命令能够提供某个时刻集合中的所有元素,但 SCAN 系列命令对返回的元素只提供有限的保证,因为我们在增量迭代的集合在迭代过程中可能会发生变化。

请注意,SCANSSCANHSCANZSCAN 的工作方式都非常相似,因此本文档涵盖了这四个命令。然而,一个明显的区别是,在 SSCANHSCANZSCAN 的情况下,第一个参数是持有 Set、Hash 或 Sorted Set 值的键名。SCAN 命令不需要任何键名参数,因为它迭代当前数据库中的键,因此迭代对象是数据库本身。

SCAN 基本用法

SCAN 是一个基于游标的迭代器。这意味着在每次调用命令时,服务器都会返回一个更新后的游标,用户需要在下一次调用中将其用作游标参数。

当游标设置为 0 时迭代开始,当服务器返回的游标为 0 时迭代终止。以下是 SCAN 迭代的示例

127.0.0.1:6379> scan 0
1) "17"
2)  1) "key:12"
    2) "key:8"
    3) "key:4"
    4) "key:14"
    5) "key:16"
    6) "key:17"
    7) "key:15"
    8) "key:10"
    9) "key:3"
   10) "key:7"
   11) "key:1"
127.0.0.1:6379> scan 17
1) "0"
2) 1) "key:5"
   2) "key:18"
   3) "key:0"
   4) "key:2"
   5) "key:19"
   6) "key:13"
   7) "key:6"
   8) "key:9"
   9) "key:11"

在上面的示例中,第一次调用使用零作为游标来开始迭代。第二次调用使用前一次调用返回的游标作为回复的第一个元素,即 17。

如您所见,SCAN 返回值是一个包含两个值的数组:第一个值是下次调用要使用的新游标,第二个值是一个包含元素数组的数组。

由于在第二次调用中返回的游标是 0,服务器向调用者发出信号,表示迭代已完成,集合已完全探索。以游标值 0 开始迭代,并持续调用 SCAN 直到返回的游标再次为 0 的过程称为完整迭代

返回值

SCANSSCANHSCANZSCAN 返回一个包含两个元素的批量回复,其中第一个元素是表示无符号 64 位数字(游标)的字符串,第二个元素是一个包含元素数组的批量回复。

  • SCAN 返回的元素数组是一个键列表。
  • SSCAN 返回的元素数组是一个 Set 成员列表。
  • HSCAN 返回的元素数组包含两个元素,即哈希中每个返回元素的字段和值。
  • ZSCAN 返回的元素数组包含两个元素,即有序集合中每个返回元素的成员及其关联分数。

Scan 保证

SCAN 命令以及 SCAN 系列中的其他命令能够为用户提供与完整迭代相关的一组保证。

  • 完整迭代总是检索在完整迭代开始到结束期间集合中存在的所有元素。这意味着,如果在迭代开始时某个给定元素在集合中,并且在迭代终止时它仍然存在,那么在某个时刻 SCAN 会将其返回给用户。
  • 完整迭代绝不会返回在完整迭代开始到结束期间不在集合中的任何元素。因此,如果一个元素在迭代开始前被删除,并且在整个迭代期间从未被添加回集合,SCAN 确保这个元素永远不会被返回。

然而,由于 SCAN 关联的状态非常少(只有游标),它具有以下缺点:

  • 给定元素可能会多次返回。应用程序需要处理重复元素的情况,例如只使用返回的元素执行可以安全地多次重新应用的操作。
  • 在完整迭代期间未持续存在于集合中的元素,可能会返回也可能不返回:这是不确定的。

每次 SCAN 调用返回的元素数量

SCAN 系列函数不保证每次调用返回的元素数量在一个给定范围内。命令也允许返回零个元素,只要返回的游标不是零,客户端就不应认为迭代已完成。

然而,返回的元素数量是合理的,也就是说,在实际操作中,当迭代大型集合时,SCAN 每次可能返回最多几十个元素;当迭代的集合足够小,可以在内部表示为编码数据结构时(对于小型 Set、Hash 和 Sorted Set 会发生这种情况),它可以在一次调用中返回集合中的所有元素。

但是,用户可以通过使用 COUNT 选项来调整每次调用返回元素数量的量级。

COUNT 选项

尽管 SCAN 不保证每次迭代返回的元素数量,但可以使用 COUNT 选项凭经验调整 SCAN 的行为。基本上,通过 COUNT,用户指定了每次调用为了从集合中检索元素而应完成的工作量。这只是一个提示,但通常情况下,您大部分时间都会从实现中得到这样的结果。

  • 默认的 COUNT 值为 10。
  • 当迭代键空间,或迭代足够大以至于可以用哈希表表示的 Set、Hash 或 Sorted Set 时,假设没有使用 MATCH 选项,服务器通常每次调用会返回 count 或稍多于 count 的元素。请查阅本文档稍后“为什么 SCAN 可能会一次性返回所有元素”一节。
  • 当迭代编码为 intset(仅由整数构成的小型集合)的 Set,或编码为 ziplist(由小型独立值构成的小型哈希和集合)的 Hash 和 Sorted Set 时,通常所有元素都会在第一次 SCAN 调用中返回,无论 COUNT 值是多少。

重要提示:每次迭代不需要使用相同的 COUNT 值。只要下一次调用中传递的游标是前一次命令调用中获得的游标,调用者可以根据需要自由更改每次迭代的计数。

MATCH 选项

只迭代与给定 glob 风格模式匹配的元素是可能的,这类似于 KEYS 命令的行为,该命令将其模式作为唯一参数。

为此,只需在 SCAN 命令的末尾添加 MATCH <pattern> 参数(它适用于所有 SCAN 系列命令)。

这是一个使用 MATCH 迭代的示例

127.0.0.1:6379> sadd myset 1 2 3 foo foobar feelsgood
(integer) 6
127.0.0.1:6379> sscan myset 0 match f*
1) "0"
2) 1) "foo"
   2) "feelsgood"
   3) "foobar"
127.0.0.1:6379>

值得注意的是,MATCH 过滤器是在从集合中检索元素之后,在将数据返回给客户端之前应用的。这意味着如果模式匹配集合中很少的元素,SCAN 在大多数迭代中很可能不会返回任何元素。下面是一个示例:

127.0.0.1:6379> scan 0 MATCH *11*
1) "288"
2) 1) "key:911"
127.0.0.1:6379> scan 288 MATCH *11*
1) "224"
2) (empty list or set)
127.0.0.1:6379> scan 224 MATCH *11*
1) "80"
2) (empty list or set)
127.0.0.1:6379> scan 80 MATCH *11*
1) "176"
2) (empty list or set)
127.0.0.1:6379> scan 176 MATCH *11* COUNT 1000
1) "0"
2)  1) "key:611"
    2) "key:711"
    3) "key:118"
    4) "key:117"
    5) "key:311"
    6) "key:112"
    7) "key:111"
    8) "key:110"
    9) "key:113"
   10) "key:211"
   11) "key:411"
   12) "key:115"
   13) "key:116"
   14) "key:114"
   15) "key:119"
   16) "key:811"
   17) "key:511"
   18) "key:11"
127.0.0.1:6379>

如您所见,大多数调用都返回零个元素,但最后一次调用中使用了 1000 的 COUNT 值,以强制命令在该次迭代中执行更多扫描。

在使用 Valkey Cluster 时,搜索针对只涉及单个槽位的模式进行了优化。如果一个模式只能匹配一个槽位中的键,那么 Valkey 在搜索匹配模式的键时,只会迭代该槽位中的键,而不是整个数据库。例如,对于模式 {a}h*llo,Valkey 只会尝试匹配槽位 15495 中的键,该槽位由哈希标签 {a} 所隐含。要使用带哈希标签的模式,请参阅集群规范中的 哈希标签 以获取更多信息。

TYPE 选项

您可以使用 !TYPE 选项,要求 SCAN 只返回与给定 type 匹配的对象,从而允许您迭代数据库以查找特定类型的键。TYPE 选项仅适用于整个数据库的 SCAN,不适用于 HSCANZSCAN 等。

type 参数与 TYPE 命令返回的字符串名称相同。请注意一个怪癖,一些 Valkey 类型,例如 GeoHashes、HyperLogLogs、Bitmaps 和 Bitfields,可能在内部使用其他 Valkey 类型(例如字符串或 zset)实现,因此无法通过 SCAN 将它们与同类型的其他键区分开来。例如,ZSET 和 GEOHASH

127.0.0.1:6379> GEOADD geokey 0 0 value
(integer) 1
127.0.0.1:6379> ZADD zkey 1000 value
(integer) 1
127.0.0.1:6379> TYPE geokey
zset
127.0.0.1:6379> TYPE zkey
zset
127.0.0.1:6379> SCAN 0 TYPE zset
1) "0"
2) 1) "geokey"
   2) "zkey"

值得注意的是,TYPE 过滤器也是在从数据库中检索元素之后应用的,因此该选项不会减少服务器完成完整迭代所需的工作量,对于稀有类型,您在多次迭代中可能不会收到任何元素。

NOVALUES 选项

在使用 HSCAN 时,您可以使用 NOVALUES 选项让 Valkey 只返回哈希表中的键而不返回它们对应的值。

127.0.0.1:6379> HSET myhash a 1 b 2
(integer) 2
127.0.0.1:6379> HSCAN myhash 0
1) "0"
2) 1) "a"
   2) "1"
   3) "b"
   4) "2"
127.0.0.1:6379> HSCAN myhash 0 NOVALUES
1) "0"
2) 1) "a"
   2) "b"

NOSCORES 选项

在使用 ZSCAN 时,您可以使用 NOSCORES 选项让 Valkey 只返回有序集合中的成员而不返回它们对应的分数。

127.0.0.1:6379> ZADD myzset 1 a 2 b
(integer) 2
127.0.0.1:6379> ZSCAN myzset 0
1) "0"
2) 1) "a"
   2) "1"
   3) "b"
   4) "2"
127.0.0.1:6379> ZSCAN myzset 0 NOSCORES
1) "0"
2) 1) "a"
   2) "b"

多个并行迭代

无限数量的客户端可以同时迭代相同的集合,因为迭代器的完整状态在游标中,每次调用都会获取并返回给客户端。服务器端不占用任何状态。

中途终止迭代

由于服务器端没有状态,所有状态都由游标捕获,调用者可以自由地中途终止迭代,而无需以任何方式向服务器发出信号。可以启动无限数量的迭代,并且永不终止,没有任何问题。

使用损坏的游标调用 SCAN

使用损坏、负数、超出范围或以其他方式无效的游标调用 SCAN 将导致未定义的行为,但绝不会导致崩溃。未定义的是,SCAN 实现无法再保证返回元素的正确性。

唯一有效的游标是:

  • 开始迭代时游标值为 0。
  • 上次调用 SCAN 返回的游标,用于继续迭代。

终止保证

只有当迭代集合的大小限制在给定的最大值时,SCAN 算法才能保证终止,否则迭代一个持续增长的集合可能会导致 SCAN 永远无法完成一次完整迭代。

这很容易直观地理解:如果集合增长,要访问所有可能的元素就需要越来越多的工作,而迭代终止的能力取决于 SCAN 的调用次数及其 COUNT 选项值与集合增长速度的比较。

为什么 SCAN 可能会在一次调用中返回聚合数据类型的所有项?

COUNT 选项文档中,我们指出有时这个命令系列可能会在一次调用中返回 Set、Hash 或 Sorted Set 的所有元素,无论 COUNT 选项值是多少。发生这种情况的原因是,只有当我们要扫描的聚合数据类型表示为哈希表时,基于游标的迭代器才能实现并发挥作用。然而,Valkey 使用了一种内存优化,其中小型聚合数据类型,在达到给定数量的项或给定最大单个元素大小之前,使用紧凑的单分配打包编码表示。在这种情况下,SCAN 没有有意义的游标可以返回,并且必须一次性迭代整个数据结构,因此其唯一合理的行为是在一次调用中返回所有内容。

然而,一旦数据结构变大并被提升为使用真正的哈希表,SCAN 命令系列将恢复正常行为。请注意,由于这种返回所有元素的特殊行为仅适用于小型聚合类型,因此它对命令的复杂性或延迟没有影响。但是,转换为真实哈希表的精确限制是用户可配置的,因此您在一次调用中可以返回的最大元素数量取决于聚合数据类型在仍使用打包表示时可能有多大。

另请注意,此行为是 SSCANHSCANZSCAN 所特有的。SCAN 本身从不表现出这种行为,因为键空间始终由哈希表表示。

延伸阅读

有关键管理的更多信息,请参阅 Valkey 键空间 教程。

其他示例

哈希值的迭代。

127.0.0.1:6379> hmset hash name Jack age 33
OK
127.0.0.1:6379> hscan hash 0
1) "0"
2) 1) "name"
   2) "Jack"
   3) "age"
   4) "33"

RESP2/RESP3 回复

数组回复:具体来说,是一个包含两个元素的数组。

  • 第一个元素是批量字符串回复,表示一个无符号的 64 位数字,即游标。

  • 第二个元素是数组回复,其中包含扫描到的键的名称。

历史

版本 更改
6.0.0

添加了 TYPE 子命令。