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

    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. 11) "key::14"
    13. 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 * 命令调用的一个迭代版本。但是通过使用可选的 选项,我们同样可以让 SCAN 命令只返回与给定全局匹配符相匹配的键:

    带有 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. 3) "user::99"
    13. 4) "user::51"
    14.  
    15. redis> SCAN 0 MATCH key::* COUNT 100
    16. 1) "214"
    17. 2) 1) "key::43"
    18. 2) "key::s"
    19. 3) "key::83"
    20. -- 其他键……
    21. 50) "key::28"
    22. 51) "key::34"

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

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

    数据结构迭代命令

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

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

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

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

    1. 散列迭代命令

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

    除了需要指定被迭代的散列之外,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 有序集合进行迭代:

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

    4. 迭代命令的共通性质

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

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

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