SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

Redis SCAN 命令及其相关命令 SSCANHSCANZSCAN 命令都是用于增量遍历集合中的元素。

  • SCAN 用于遍历当前数据库中的键。
  • SSCAN 用于遍历集合键中的元素。
  • HSCAN 用于遍历哈希键中的键值对。
  • ZSCAN 用于遍历有序集合中的元素(包括元素成员和元素分值)。

*语法

redis SCAN 命令基本语法如下:

SCAN cursor [MATCH pattern] [COUNT count]
  • cursor - 游标。
  • pattern - 匹配的模式。
  • count - 指定从数据集里返回多少元素,默认值为 10 。

以上列出的四个命令都支持增量式遍历, 它们每次执行都只会返回少量元素, 所以这些命令可以用于生产环境, 而不会出现像 KEYS 命令、 SMEMBERS 命令带来的问题 —— 当 KEYS 命令被用于处理一个大的数据库时, 又或者 SMEMBERS 命令被用于处理一个大的集合键时, 它们可能会阻塞服务器达数秒之久。

不过, 增量式遍历命令也不是没有缺点的: 举个例子, 使用 SMEMBERS 命令可以返回集合键某一时刻包含的所有元素, 但是对于 SCAN 这类增量式遍历命令来说, 因为在对键进行增量式遍历的过程中, 键可能会被修改, 所以增量式遍历命令不能完全保证返回所有元素。

因为 SCANSSCANHSCANZSCAN 四个命令的工作方式都非常相似, 所以本文一并介绍这四个命令, 但是要记住:SSCAN 命令、 HSCAN 命令和 ZSCAN 命令的第一个参数总是一个存储集合的键名。而 SCAN 命令则不需要在第一个参数提供任何数据库键 —— 因为它遍历的是当前数据库中的所包含的键。

*SCAN 命令的基本用法

什么是 Redis 增量遍历?SCAN 命令是一个基于游标的遍历器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次遍历时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的遍历过程。

SCAN 返回一个包含两个元素的数组, 第一个元素是用于进行下一次遍历的新游标, 而第二个元素则是一个数组, 这个数组中包含了所有被遍历的元素。当 SCAN 命令的游标参数被设置为 0 时, 服务器将开始一次新的遍历,而当服务器向用户返回值为 0 的游标时, 表示遍历已结束。例如:

redis 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"
redis 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"

在上面这个例子中, 第一次遍历使用 0 作为游标, 表示开始一次新的遍历。第二次遍历使用的是第一次遍历时返回的游标, 也就是命令回复第一个元素的值 17

*SCAN 命令的有效性

  • 如果有一个元素, 它从遍历开始直到遍历结束期间都存在于被遍历的数据集当中, 那么 SCAN 命令总会在某次遍历中将这个元素返回给用户。
  • 如果有一个元素, 它从遍历开始就已经被删除,且直到遍历结束也没有被添加回来, 那么 SCAN 命令确保不会返回这个元素。

然而因为 SCAN 命令仅仅使用游标来记录遍历状态, 所以这些命令带有以下缺点:

  • 同一个元素可能会被返回多次。 处理重复元素的工作交由应用程序负责, 比如说, 可以考虑将遍历返回的元素,只用于可以安全地重复执行多次的操作上。
  • 如果一个元素是在遍历过程中被添加到数据集的, 又或者是在遍历过程中从数据集中被删除的, 那么这个元素可能会被返回, 也可能不会, 这是不确定的。

*SCAN 命令每次执行返回的元素数量

SCAN 命令族并不保证每次执行都返回某个给定数量的元素。增量式命令甚至可能会返回零个元素, 但只要命令返回的游标不是 0 , 应用程序就不应该将遍历视作结束。

不过命令返回的元素数量总是符合一定规则的, 在实际中:对于一个大数据集来说, 增量式遍历命令每次最多可能会返回数十个元素;而对于一个足够小的数据集来说, 小集合键、小哈希键和小有序集合键, 那么增量遍历命令将在一次调用中返回数据集中的所有元素。

最后, 用户可以通过增量式遍历命令提供的 COUNT 选项来指定每次遍历返回元素的最大值。

*COUNT 选项

虽然 SCAN 命令不保证每次遍历所返回的元素数量, 但我们可以使用 COUNT 选项, 对命令的行为进行一定程度上的调整。 COUNT 选项的作用就是让用户告知遍历命令, 在每次遍历中应该从数据集里返回多少元素。虽然这个选项只是对增量式遍历命令的一种提示, 但是在大多数情况下, 这种提示都是有效的。

  • COUNT 参数的默认值为 10
  • 在遍历一个足够大的、由哈希表实现的数据库、集合键、哈希键或者有序集合键时, 如果用户没有使用 MATCH 选项, 那么命令返回的元素数量通常和 COUNT 选项指定的一样, 或者比 COUNT 选项指定的数量稍多一些。
  • 在遍历一个编码为整数集合(intset,一个只由整数值构成的小集合)、 或者编码为压缩列表(ziplist,由不同值构成的一个小哈希或者一个小有序集合)时, 增量式遍历命令通常会无视 COUNT 选项指定的值, 在第一次遍历就将数据集包含的所有元素都返回给用户。

重要: 并非每次遍历都要使用相同的 COUNT 值。用户可以在每次遍历中按自己的需要随意改变 COUNT 值, 只要记得将上次遍历返回的游标用到下次遍历里面就可以了。

*MATCH 选项

KEYS 命令一样, SCAN 命令族也可以通过提供一个 glob 风格的模式参数, 让命令只返回和给定模式相匹配的元素, 可以通过在执行增量式遍历命令时, 通过给定 MATCH <pattern> 参数来实现。

例如:

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

对元素的模式匹配工作是在命令从数据集中取出元素之后, 向客户端返回元素之前的这段时间内进行的, 所以如果被遍历的数据集中只有少量元素和模式相匹配, 那么遍历命令或许会在多次执行中都不返回任何元素。例如:

redis 127.0.0.1:6379> scan 0 MATCH *11*
1) "288"
2) 1) "key:911"
redis 127.0.0.1:6379> scan 288 MATCH *11*
1) "224"
2) (empty list or set)
redis 127.0.0.1:6379> scan 224 MATCH *11*
1) "80"
2) (empty list or set)
redis 127.0.0.1:6379> scan 80 MATCH *11*
1) "176"
2) (empty list or set)
redis 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"
redis 127.0.0.1:6379>

我们可以看到, 以上的大部分遍历都不返回任何元素。

在最后一次遍历, 我们通过将 COUNT 选项的参数设置为 1000 , 强制命令为本次遍历扫描更多元素, 从而使得命令返回的元素也变多了。

*TYPE 选项

从6.0版开始,您可以使用 TYPE 选项要求 SCAN 只返回与给定类型匹配的对象,从而允许您在数据库中迭代查找特定类型的键。TYPE选项仅在整个数据库SCAN 命令上可用,HSCANZSCAN 等无效。

注意一个奇怪的地方,一些Redis类型,如GeoHashes、HyperLogLogs、Bitmaps和Bitfields,可能在内部使用其他Redis类型(如字符串或zset)来实现, TYPE 命令返回的类型相同。因此无法通过类型 SCAN 来区分。例如,ZSET 和 GEOHASH:

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

请务必注意,TYPE 筛选器会在从数据库中获取到元素后应用,因此该选项不会减少服务器完成完整遍历所必须执行的工作量,对于罕见类型,您可能在多次遍历中没有收到任何元素。

*并发执行多个遍历

在同一时间, 可以有任意多个客户端对同一数据集进行遍历, 客户端每次执行遍历都需要传入一个游标, 并在遍历执行之后获得一个新的游标, 而这个游标就包含了遍历的所有状态, 因此, 服务器无须为遍历记录任何状态。

*中途停止遍历

因为遍历的所有状态都保存在游标里面, 而服务器无须为遍历保存任何状态, 所以客户端可以在中途停止一个遍历, 而无须对服务器进行任何通知。即使有任意数量的遍历在中途停止, 也不会产生任何问题。

*使用错误的游标进行增量式遍历

SCAN 使用间断的、负数、超出范围或者其他非正常的游标来执行增量式遍历并不会造成服务器崩溃, 但可能会让命令产生不确定的结果。

只有两种游标是合法的:

  • 在开始一个新的遍历时, 游标必须为 0
  • 增量式遍历命令在执行之后返回的游标值, 用于延续(continue)遍历过程的游标。

*遍历结束的保证

SCAN 命令所使用的算法只保证在数据集的大小有界(bounded)的情况下, 遍历才会停止, 换句话说, 如果被遍历数据集的大小不断地增长的话, 增量式遍历命令可能永远也无法完成一次完整遍历。

从直觉上可以看出, 当一个数据集不断地变大时, 想要访问这个数据集中的所有元素就需要做越来越多的工作, 能否结束一个遍历取决于用户执行遍历的速度是否比数据集增长的速度更快。

*返回值

SCAN, SSCAN, HSCAN and ZSCAN 命令都返回一个包含两个元素的回复: 第一个元素是字符串表示的无符号 64 位整数(游标), 第二个元素是本次被遍历的元素数组。

  • SCAN key 数组。
  • SSCAN 集合成员的数组。
  • HSCAN HASH 键值对数组,一个键值对由一个键和一个值组成。
  • ZSCAN 元素数组,每个元素都是一个有序集合元素,一个有序集合元素由一个成员(member)和一个分值(score)组成。

*历史

  • >= 6.0: 支持 TYPE 子命令。

*额外例子

遍历 hash 值。

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