SCAN:以渐进方式迭代数据库中的键
因为 KEYS
命令需要检查数据库包含的所有键,并一次性将符合条件的所有键全部返回给客户端,所以当数据库包含的键数量比较大时,使用 KEYS
命令可能会导致服务器被阻塞。
为了解决这个问题,Redis 从 2.8.0 版本开始提供 SCAN
命令,该命令是一个迭代器,它每次被调用的时候都会从数据库里面获取一部分键,用户可以通过重复调用 SCAN
命令来迭代数据库包含的所有键:
- SCAN cursor
SCAN
命令的 cursor
参数用于指定迭代时使用的游标,游标记录了迭代进行的轨迹和进度。在开始一次新的迭代时,用户需要将游标设置为 0
:
- SCAN 0
SCAN
命令的执行结果由两个元素组成:
第一个元素是进行下一次迭代所需的游标,如果这个游标为
0
,那么说明客户端已经对数据库完成了一次完整的迭代。第二个元素是一个列表,这个列表包含了本次迭代取得的数据库键;如果
SCAN
命令在某次迭代中没有获取到任何键,那么这个元素将是一个空列表。
关于 SCAN
命令返回的键列表,有两个需要注意的地方:
SCAN
命令可能会返回重复的键,用户如果不想在结果里面包含重复的键,那么就需要自己在客户端里面进行检测和过滤。SCAN
命令返回的键数量是不确定的,有时候甚至会不返回任何键,但只要命令返回的游标不为0
,迭代就没有结束。
一次简单的迭代示例
在对 SCAN
命令有了基本的了解之后,让我们来试试使用 SCAN
命令去完整地迭代一个数据库。
为了开始一次新的迭代,我们将以 0
作为游标,调用 SCAN
命令:
- redis> SCAN 0
- 1) "25" -- 进行下次迭代的游标
- 2) 1) "key::16" -- 本次迭代获取到的键
- 2) "key::2"
- 3) "key::6"
- 4) "key::8"
- 5) "key::13"
- 6) "key::22"
- 7) "key::10"
- 8) "key::24"
- 9) "key::23"
- 10) "key::21"
- 11) "key::5"
这个 SCAN
调用告知我们下次迭代应该使用 25
作为游标,并返回了十一个键的键名。
为了继续对数据库进行迭代,我们使用 25
作为游标,再次调用 SCAN
命令:
- redis> SCAN 25
- 1) "31"
- 2) 1) "key::20"
- 2) "key::18"
- 3) "key::19"
- 4) "key::7"
- 5) "key::1"
- 6) "key::9"
- 7) "key::12"
- 8) "key::11"
- 9) "key::17"
- 10) "key::15"
- 11) "key::14"
- 12) "key::3"
这次的 SCAN
调用返回了十二个键,并告知我们下次迭代应该使用 31
作为游标。
跟之前的情况类似,这次我们使用 31
作为游标,再次调用 SCAN
命令:
- redis> SCAN 31
- 1) "0"
- 2) 1) "key::0"
- 2) "key::4"
这次的 SCAN
调用只返回了两个键,并且它返回的下次迭代游标为 0
——这说明本次迭代已经结束,整个数据库已经被迭代完毕。
SCAN 命令的迭代保证
针对数据库的一次完整迭代(full iteration)以用户给定游标 0
调用 SCAN
命令为开始,直到 SCAN
命令返回游标 0
为结束。SCAN
命令为完整迭代提供以下保证:
从迭代开始到迭代结束的整个过程中,一直存在于数据库里面的键总会被返回。
如果一个键在迭代的过程中被添加到了数据库里面,那么这个键是否会被返回是不确定的。
如果一个键在迭代的过程中被移除了,那么
SCAN
命令在它被移除之后将不再返回这个键;但是这个键在被移除之前仍然有可能被SCAN
命令返回。无论数据库如何变化,迭代总是有始有终的,不会出现循环迭代或者其他无法终止迭代的情况。
游标的使用
在很多数据库里面,使用游标都要显式地进行申请,并在迭代完成之后释放游标,否则的话就会造成内存泄露。
与此相反,SCAN
命令的游标不需要申请,也不需要释放,它们不占用任何资源,每个客户端都可以使用自己的游标独立地对数据库进行迭代。
此外,用户可以随时在迭代的途中停止进行迭代,又或者随时开始一次新的迭代,这不会浪费任何资源,也不会引发任何问题。
迭代与给定匹配符相匹配的键
在默认情况下,SCAN
命令会向客户端返回数据库包含的所有键,它就像 KEYS *
命令调用的一个迭代版本。但是通过使用可选的 MATCH
选项,我们同样可以让 SCAN
命令只返回与给定全局匹配符相匹配的键:
- SCAN cursor [MATCH pattern]
带有 MATCH
选项的 SCAN
命令就像是 KEYS pattern
命令调用的迭代版本。
举个例子,假设我们想要获取数据库里面所有以 user::
开头的键,但是因为这些键的数量比较多,直接使用 KEYS user::
有可能会造成服务器阻塞,所以我们可以使用 SCAN
命令来代替 KEYS
命令,对符合 user::
匹配符的键进行迭代:
- redis> SCAN 0 MATCH user::*
- 1) "208"
- 2) 1) "user::1"
- 2) "user::65"
- 3) "user::99"
- 4) "user::51"
- redis> SCAN 208 MATCH user::*
- 1) "232"
- 2) 1) "user::13"
- 2) "user::28"
- 3) "user::83"
- 4) "user::14"
- 5) "user::61"
- -- 省略后续的其他迭代……
指定返回键的期望数量
在一般情况下,SCAN
命令返回的键数量是不确定的,但是我们可以通过使用可选的 COUNT
选项,向 SCAN
命令提供一个期望值,以此来说明我们希望得到多少个键:
- SCAN cursor [COUNT number]
这里特别需要注意的是,COUNT
选项向命令提供的只是期望的键数量,但并不是精确的键数量。比如说,执行 SCAN cursor COUNT 10
并不是说 SCAN
命令最多只能返回 10
个键,又或者一定要返回 10
个键:
COUNT
选项只是提供了一个期望值,告诉SCAN
命令我们希望返回多少个键,但每次迭代返回的键数量仍然是不确定的。不过在通常情况下,设置一个较大的
COUNT
值将有助于获得更多键,这一点是可以肯定的。
以下代码展示了几个使用 COUNT
选项的例子:
- redis> SCAN 0 COUNT 5
- 1) "160"
- 2) 1) "key::43"
- 2) "key::s"
- 3) "user::1"
- 4) "key::83"
- 5) "key::u"
- redis> SCAN 0 MATCH user::* COUNT 10
- 1) "208"
- 2) 1) "user::1"
- 2) "user::65"
- 3) "user::99"
- 4) "user::51"
- redis> SCAN 0 MATCH key::* COUNT 100
- 1) "214"
- 2) 1) "key::43"
- 2) "key::s"
- 3) "key::83"
- -- 其他键……
- 50) "key::28"
- 51) "key::34"
在用户没有显式地使用 COUNT
选项的情况下,SCAN
命令将使用 10
作为 COUNT
选项的默认值,换句话说,以下两条命令的作用是相同的:
- SCAN cursor
- SCAN cursor COUNT 10
数据结构迭代命令
跟获取数据库键的 KEYS
命令一样,Redis 的各个数据结构也存在着一些可能会导致服务器阻塞的命令:
散列的
HKEYS
命令、HVALS
命令和HGETALL
命令在处理包含键值对较多的散列时,可能会导致服务器阻塞。集合的
SMEMBERS
命令在处理包含元素较多的集合时,可能会导致服务器阻塞。有序集合的一些范围型获取命令,比如
ZRANGE
,也有阻塞服务器的可能。比如说,为了获取有序集合包含的所有元素,用户可能会执行命令调用ZRANGE key 0 -1
,这时如果有序集合包含的成员数量较多的话,这个ZRANGE
命令可能就会导致服务器阻塞。
为了解决以上这些问题,Redis 为散列、集合和有序集合也提供了与 SCAN
命令类似的游标迭代命令,它们分别是 HSCAN
命令、 SSCAN
命令和 ZSCAN
命令,以下三个小节将分别介绍这三个命令的用法。
1. 散列迭代命令
HSCAN
命令可以以渐进的方式迭代给定散列包含的键值对:
- HSCAN hash cursor [MATCH pattern] [COUNT number]
除了需要指定被迭代的散列之外,HSCAN
命令的其他参数跟 SCAN
命令的参数保持一致,并且作用也一样。
作为例子,以下代码展示了如何使用 HSCAN
命令去迭代 user::10086::profile
散列:
- redis> HSCAN user::10086::profile 0
- 1) "0" -- 下次迭代的游标
- 2) 1) "name" -- 键
- 2) "peter" -- 值
- 3) "age"
- 4) "32"
- 5) "gender"
- 6) "male"
- 7) "blog"
- 8) "peter123.whatpress.com"
- 9) "email"
- 10) "peter123@example.com"
当散列包含较多键值对的时候,我们应该尽量使用 HSCAN
去代替 HKEYS
、 HVALS
和 HGETALL
,以免造成服务器阻塞。
2. 渐进式集合迭代命令
SSCAN
命令可以以渐进的方式迭代给定集合包含的元素:
- SSCAN set cursor [MATCH pattern] [COUNT number]
除了需要指定被迭代的集合之外,SSCAN
命令的其他参数跟 SCAN
命令的参数保持一致,并且作用也一样。
举个例子,假设我们想要对 fruits
集合进行迭代的话,那么可以执行以下命令:
- redis> SSCAN fruits 0
- 1) "0" -- 下次迭代的游标
- 2) 1) "apple" -- 集合元素
- 2) "watermelon"
- 3) "mango"
- 4) "cherry"
- 5) "banana"
- 6) "dragon fruit"
当集合包含较多元素的时候,我们应该尽量使用 SSCAN
去代替 SMEMBERS
,以免造成服务器阻塞。
3. 渐进式有序集合迭代命令
ZSCAN
命令可以以渐进的方式迭代给定有序集合包含的成员和分值:
- ZSCAN sorted_set cursor [MATCH pattern] [COUNT number]
除了需要指定被迭代的有序集合之外,ZSCAN
命令的其他参数跟 SCAN
命令的参数保持一致,并且作用也一样。
比如说,通过执行以下命令,我们可以对 fruits-price
有序集合进行迭代:
- redis> ZSCAN fruits-price 0
- 1) "0" -- 下次迭代的游标
- 2) 1) "watermelon" -- 成员
- 2) "3.5" -- 分值
- 3) "banana"
- 4) "4.5"
- 5) "mango"
- 6) "5"
- 7) "dragon fruit"
- 8) "6"
- 9) "cherry"
- 10) "7"
- 11) "apple"
- 12) "8.5"
当有序集合包含较多成员的时候,我们应该尽量使用 ZSCAN
去代替 ZRANGE
以及其他可能会返回大量成员的范围型获取命令,以免造成服务器阻塞。
4. 迭代命令的共通性质
HSCAN
、 SSCAN
、 ZSCAN
这三个命令除了与 SCAN
命令拥有相同的游标参数以及可选项之外,还与 SCAN
命令拥有相同的迭代性质:
SCAN
命令对于完整迭代所做的保证,其他三个迭代命令也能够提供。比如说,使用HSCAN
命令对散列进行一次完整迭代,在迭代过程中一直存在的键值对总会被返回,诸如此类。跟
SCAN
命令一样,其他三个迭代命令的游标也不耗费任何资源。用户可以在这三个命令中随意地使用游标,比如随时开始一次新的迭代,又或者随时放弃正在进行的迭代,这不会浪费任何资源,也不会引发任何问题。跟
SCAN
命令一样,其他三个迭代命令虽然也可以使用COUNT
选项设置返回元素数量的期望值,但命令具体返回的元素数量仍然是不确定的。
其他信息
属性 | 值 |
---|---|
复杂度 | SCAN 命令、 HSCAN 命令、 SSCAN 命令和 ZSCAN 命令单次执行的复杂度为 O(1) ,而使用这些命令进行一次完整迭代的复杂度则为 O(N) ,其中 N 为被迭代的元素数量。 |
版本要求 | SCAN 命令、 HSCAN 命令、 SSCAN 命令和 ZSCAN 命令从 Redis 2.8.0 版本开始可用。 |