SCAN:以渐进方式迭代数据库中的键

因为 KEYS 命令需要检查数据库包含的所有键,并一次性将符合条件的所有键全部返回给客户端,所以当数据库包含的键数量比较大时,使用 KEYS 命令可能会导致服务器被阻塞。

为了解决这个问题,Redis 从 2.8.0 版本开始提供 SCAN 命令,该命令是一个迭代器,它每次被调用的时候都会从数据库里面获取一部分键,用户可以通过重复调用 SCAN 命令来迭代数据库包含的所有键:

  1. SCAN cursor

SCAN 命令的 cursor 参数用于指定迭代时使用的游标,游标记录了迭代进行的轨迹和进度。在开始一次新的迭代时,用户需要将游标设置为 0

  1. SCAN 0

SCAN 命令的执行结果由两个元素组成:

  • 第一个元素是进行下一次迭代所需的游标,如果这个游标为 0 ,那么说明客户端已经对数据库完成了一次完整的迭代。

  • 第二个元素是一个列表,这个列表包含了本次迭代取得的数据库键;如果 SCAN 命令在某次迭代中没有获取到任何键,那么这个元素将是一个空列表。

关于 SCAN 命令返回的键列表,有两个需要注意的地方:

  • SCAN 命令可能会返回重复的键,用户如果不想在结果里面包含重复的键,那么就需要自己在客户端里面进行检测和过滤。

  • SCAN 命令返回的键数量是不确定的,有时候甚至会不返回任何键,但只要命令返回的游标不为 0 ,迭代就没有结束。

一次简单的迭代示例

在对 SCAN 命令有了基本的了解之后,让我们来试试使用 SCAN 命令去完整地迭代一个数据库。

为了开始一次新的迭代,我们将以 0 作为游标,调用 SCAN 命令:

  1. redis> SCAN 0
  2. 1) "25" -- 进行下次迭代的游标
  3. 2) 1) "key::16" -- 本次迭代获取到的键
  4. 2) "key::2"
  5. 3) "key::6"
  6. 4) "key::8"
  7. 5) "key::13"
  8. 6) "key::22"
  9. 7) "key::10"
  10. 8) "key::24"
  11. 9) "key::23"
  12. 10) "key::21"
  13. 11) "key::5"

这个 SCAN 调用告知我们下次迭代应该使用 25 作为游标,并返回了十一个键的键名。

为了继续对数据库进行迭代,我们使用 25 作为游标,再次调用 SCAN 命令:

  1. redis> SCAN 25
  2. 1) "31"
  3. 2) 1) "key::20"
  4. 2) "key::18"
  5. 3) "key::19"
  6. 4) "key::7"
  7. 5) "key::1"
  8. 6) "key::9"
  9. 7) "key::12"
  10. 8) "key::11"
  11. 9) "key::17"
  12. 10) "key::15"
  13. 11) "key::14"
  14. 12) "key::3"

这次的 SCAN 调用返回了十二个键,并告知我们下次迭代应该使用 31 作为游标。

跟之前的情况类似,这次我们使用 31 作为游标,再次调用 SCAN 命令:

  1. redis> SCAN 31
  2. 1) "0"
  3. 2) 1) "key::0"
  4. 2) "key::4"

这次的 SCAN 调用只返回了两个键,并且它返回的下次迭代游标为 0 ——这说明本次迭代已经结束,整个数据库已经被迭代完毕。

SCAN 命令的迭代保证

针对数据库的一次完整迭代(full iteration)以用户给定游标 0 调用 SCAN 命令为开始,直到 SCAN 命令返回游标 0 为结束。SCAN 命令为完整迭代提供以下保证:

  • 从迭代开始到迭代结束的整个过程中,一直存在于数据库里面的键总会被返回。

  • 如果一个键在迭代的过程中被添加到了数据库里面,那么这个键是否会被返回是不确定的。

  • 如果一个键在迭代的过程中被移除了,那么 SCAN 命令在它被移除之后将不再返回这个键;但是这个键在被移除之前仍然有可能被 SCAN 命令返回。

  • 无论数据库如何变化,迭代总是有始有终的,不会出现循环迭代或者其他无法终止迭代的情况。

游标的使用

在很多数据库里面,使用游标都要显式地进行申请,并在迭代完成之后释放游标,否则的话就会造成内存泄露。

与此相反,SCAN 命令的游标不需要申请,也不需要释放,它们不占用任何资源,每个客户端都可以使用自己的游标独立地对数据库进行迭代。

此外,用户可以随时在迭代的途中停止进行迭代,又或者随时开始一次新的迭代,这不会浪费任何资源,也不会引发任何问题。

迭代与给定匹配符相匹配的键

在默认情况下,SCAN 命令会向客户端返回数据库包含的所有键,它就像 KEYS * 命令调用的一个迭代版本。但是通过使用可选的 MATCH 选项,我们同样可以让 SCAN 命令只返回与给定全局匹配符相匹配的键:

  1. SCAN cursor [MATCH pattern]

带有 MATCH 选项的 SCAN 命令就像是 KEYS pattern 命令调用的迭代版本。

举个例子,假设我们想要获取数据库里面所有以 user:: 开头的键,但是因为这些键的数量比较多,直接使用 KEYS user:: 有可能会造成服务器阻塞,所以我们可以使用 SCAN 命令来代替 KEYS 命令,对符合 user:: 匹配符的键进行迭代:

  1. redis> SCAN 0 MATCH user::*
  2. 1) "208"
  3. 2) 1) "user::1"
  4. 2) "user::65"
  5. 3) "user::99"
  6. 4) "user::51"
  7.  
  8. redis> SCAN 208 MATCH user::*
  9. 1) "232"
  10. 2) 1) "user::13"
  11. 2) "user::28"
  12. 3) "user::83"
  13. 4) "user::14"
  14. 5) "user::61"
  15.  
  16. -- 省略后续的其他迭代……

指定返回键的期望数量

在一般情况下,SCAN 命令返回的键数量是不确定的,但是我们可以通过使用可选的 COUNT 选项,向 SCAN 命令提供一个期望值,以此来说明我们希望得到多少个键:

  1. SCAN cursor [COUNT number]

这里特别需要注意的是,COUNT 选项向命令提供的只是期望的键数量,但并不是精确的键数量。比如说,执行 SCAN cursor COUNT 10 并不是说 SCAN 命令最多只能返回 10 个键,又或者一定要返回 10 个键:

  • COUNT 选项只是提供了一个期望值,告诉 SCAN 命令我们希望返回多少个键,但每次迭代返回的键数量仍然是不确定的。

  • 不过在通常情况下,设置一个较大的 COUNT 值将有助于获得更多键,这一点是可以肯定的。

以下代码展示了几个使用 COUNT 选项的例子:

  1. redis> SCAN 0 COUNT 5
  2. 1) "160"
  3. 2) 1) "key::43"
  4. 2) "key::s"
  5. 3) "user::1"
  6. 4) "key::83"
  7. 5) "key::u"
  8.  
  9. redis> SCAN 0 MATCH user::* COUNT 10
  10. 1) "208"
  11. 2) 1) "user::1"
  12. 2) "user::65"
  13. 3) "user::99"
  14. 4) "user::51"
  15.  
  16. redis> SCAN 0 MATCH key::* COUNT 100
  17. 1) "214"
  18. 2) 1) "key::43"
  19. 2) "key::s"
  20. 3) "key::83"
  21. -- 其他键……
  22. 50) "key::28"
  23. 51) "key::34"

在用户没有显式地使用 COUNT 选项的情况下,SCAN 命令将使用 10 作为 COUNT 选项的默认值,换句话说,以下两条命令的作用是相同的:

  1. SCAN cursor
  2.  
  3. SCAN cursor COUNT 10

数据结构迭代命令

跟获取数据库键的 KEYS 命令一样,Redis 的各个数据结构也存在着一些可能会导致服务器阻塞的命令:

  • 散列的 HKEYS 命令、 HVALS 命令和 HGETALL 命令在处理包含键值对较多的散列时,可能会导致服务器阻塞。

  • 集合的 SMEMBERS 命令在处理包含元素较多的集合时,可能会导致服务器阻塞。

  • 有序集合的一些范围型获取命令,比如 ZRANGE ,也有阻塞服务器的可能。比如说,为了获取有序集合包含的所有元素,用户可能会执行命令调用 ZRANGE key 0 -1 ,这时如果有序集合包含的成员数量较多的话,这个 ZRANGE 命令可能就会导致服务器阻塞。

为了解决以上这些问题,Redis 为散列、集合和有序集合也提供了与 SCAN 命令类似的游标迭代命令,它们分别是 HSCAN 命令、 SSCAN 命令和 ZSCAN 命令,以下三个小节将分别介绍这三个命令的用法。

1. 散列迭代命令

HSCAN 命令可以以渐进的方式迭代给定散列包含的键值对:

  1. HSCAN hash cursor [MATCH pattern] [COUNT number]

除了需要指定被迭代的散列之外,HSCAN 命令的其他参数跟 SCAN 命令的参数保持一致,并且作用也一样。

作为例子,以下代码展示了如何使用 HSCAN 命令去迭代 user::10086::profile 散列:

  1. redis> HSCAN user::10086::profile 0
  2. 1) "0" -- 下次迭代的游标
  3. 2) 1) "name" --
  4. 2) "peter" --
  5. 3) "age"
  6. 4) "32"
  7. 5) "gender"
  8. 6) "male"
  9. 7) "blog"
  10. 8) "peter123.whatpress.com"
  11. 9) "email"
  12. 10) "peter123@example.com"

当散列包含较多键值对的时候,我们应该尽量使用 HSCAN 去代替 HKEYSHVALSHGETALL ,以免造成服务器阻塞。

2. 渐进式集合迭代命令

SSCAN 命令可以以渐进的方式迭代给定集合包含的元素:

  1. SSCAN set cursor [MATCH pattern] [COUNT number]

除了需要指定被迭代的集合之外,SSCAN 命令的其他参数跟 SCAN 命令的参数保持一致,并且作用也一样。

举个例子,假设我们想要对 fruits 集合进行迭代的话,那么可以执行以下命令:

  1. redis> SSCAN fruits 0
  2. 1) "0" -- 下次迭代的游标
  3. 2) 1) "apple" -- 集合元素
  4. 2) "watermelon"
  5. 3) "mango"
  6. 4) "cherry"
  7. 5) "banana"
  8. 6) "dragon fruit"

当集合包含较多元素的时候,我们应该尽量使用 SSCAN 去代替 SMEMBERS ,以免造成服务器阻塞。

3. 渐进式有序集合迭代命令

ZSCAN 命令可以以渐进的方式迭代给定有序集合包含的成员和分值:

  1. ZSCAN sorted_set cursor [MATCH pattern] [COUNT number]

除了需要指定被迭代的有序集合之外,ZSCAN 命令的其他参数跟 SCAN 命令的参数保持一致,并且作用也一样。

比如说,通过执行以下命令,我们可以对 fruits-price 有序集合进行迭代:

  1. redis> ZSCAN fruits-price 0
  2. 1) "0" -- 下次迭代的游标
  3. 2) 1) "watermelon" -- 成员
  4. 2) "3.5" -- 分值
  5. 3) "banana"
  6. 4) "4.5"
  7. 5) "mango"
  8. 6) "5"
  9. 7) "dragon fruit"
  10. 8) "6"
  11. 9) "cherry"
  12. 10) "7"
  13. 11) "apple"
  14. 12) "8.5"

当有序集合包含较多成员的时候,我们应该尽量使用 ZSCAN 去代替 ZRANGE 以及其他可能会返回大量成员的范围型获取命令,以免造成服务器阻塞。

4. 迭代命令的共通性质

HSCANSSCANZSCAN 这三个命令除了与 SCAN 命令拥有相同的游标参数以及可选项之外,还与 SCAN 命令拥有相同的迭代性质:

  • SCAN 命令对于完整迭代所做的保证,其他三个迭代命令也能够提供。比如说,使用 HSCAN 命令对散列进行一次完整迭代,在迭代过程中一直存在的键值对总会被返回,诸如此类。

  • SCAN 命令一样,其他三个迭代命令的游标也不耗费任何资源。用户可以在这三个命令中随意地使用游标,比如随时开始一次新的迭代,又或者随时放弃正在进行的迭代,这不会浪费任何资源,也不会引发任何问题。

  • SCAN 命令一样,其他三个迭代命令虽然也可以使用 COUNT 选项设置返回元素数量的期望值,但命令具体返回的元素数量仍然是不确定的。

其他信息

属性
复杂度SCAN 命令、 HSCAN 命令、 SSCAN 命令和 ZSCAN 命令单次执行的复杂度为 O(1) ,而使用这些命令进行一次完整迭代的复杂度则为 O(N) ,其中 N 为被迭代的元素数量。
版本要求SCAN 命令、 HSCAN 命令、 SSCAN 命令和 ZSCAN 命令从 Redis 2.8.0 版本开始可用。