【Redis】4、Scan 命令

https://redis.io/commands/scan/

一、场景

背景:keys 可以一次性根据 pattern 查出匹配的 key,但因数据量太大会有性能抖动

所以可以用 scan。

scan 的核心是用游标遍历,避免性能抖动。

二、SCAN 命令

scan 是基于游标的迭代器,每次调用后都会返回新的游标,用户在下次调用时传入新的游标。

当游标设置为 0 时开始迭代,当服务器返回的游标为 0 时终止迭代。

时间复杂度:每次调用 O(1)。完整迭代是 O(N)。

2.1 命令的用法

scan 的语法如下:

bash 复制代码
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
  • cursor 是一个数字,如 0 为迭代的开始或结束,其他值为中间过程的某游标。
  • pattern 是一个通配符,匹配 key,例如 "user*" 即为以 user 开头的字符串,例如 a*b 即为另一种模式的字符串
  • count 是一个数字,并不保证精确,基本上表示 redis server 会把游标迭代几次。(这只是实现的一个提示,大多数情况这种提示都是有效的,但数据量少的情况下并不精确)
  • type 是 key 的类型,如可以指定 string、set、zset、hash 等

和 scan 类似的,还有如下具体类型的命令(这些具体类型命令的就没有 TYPE 参数了):

2.2 案例

2.2.1 准备数据

bash 复制代码
set str1 a

sadd set2 a b c
smembers set2 # b a c (存储时就并不保证顺序)

set str3 a

zadd zset4 1 a 2 b 3 c
zrange zset4 0 -1 # a b c (按各元素的score的顺序,存储和输出)

hset hash5 a 1 b 2 c 3
hgetall # a 1 b 2 c 3

set str6 a
set str7 a
set str8 a
set str9 a
set str10 a
set str11 a
set str12 a
set str13 a
set str14 a

2.2.2 keys *

bash 复制代码
keys * # 可看到工 14 个 keys
1)  "str1"
2)  "str12"
3)  "zset4"
4)  "str3"
5)  "str6"
6)  "str9"
7)  "str14"
8)  "str8"
9)  "str7"
10) "str13"
11) "str10"
12) "set2"
13) "str11"
14) "hash5"

2.2.3 scan

2.2.3.1 仅使用 cursor 迭代
bash 复制代码
# 刚开始迭代时,从游标 0 开始
scan 0
1) "7" # 返回游了标 7,说明迭代还没完成。并返回了 11 个元素
2) 1)  "str14"
   2)  "str8"
   3)  "str12"
   4)  "str7"
   5)  "zset4"
   6)  "str11"
   7)  "hash5"
   8)  "str1"
   9)  "set2"
   10) "str13"
   11) "str10"
   
   
# 因为上次 scan 返回了游标 7,所以这次接着从 7 继续
scan 7
1) "0" # 返回了游标 0,说明迭代完成。并返回了剩余的 3 个元素
2) 1) "str3"
   2) "str6"
   3) "str9"
2.2.3.2 使用 cursor 和 type 迭代

迭代 string type

bash 复制代码
# 刚开始迭代时,从游标 0 开始
scan 0 type string
1) "7" # 返回游了标 7,说明迭代还没完成。并返回了 8 个元素
2) 1) "str14"
   2) "str8"
   3) "str12"
   4) "str7"
   5) "str11"
   6) "str1"
   7) "str13"
   8) "str10"
   
# 因为上次 scan 返回了游标 7,所以这次接着从 7 继续 
scan 7 type string
1) "0" # 返回了游标 0,说明迭代完成。并返回了剩余的 3 个元素
2) 1) "str3"
   2) "str6"
   3) "str9"

scan 0 type string
1) "7"
2) 1) "str14"
   2) "str8"
   3) "str12"
   4) "str7"
   5) "str11"
   6) "str1"
   7) "str13"
   8) "str10"

迭代 set type

bash 复制代码
scan 0 type set
1) "7"
2) 1) "set2"

scan 7 type set
1) "0"
2) null

迭代 zset type

bash 复制代码
scan 0 type zset
1) "7"
2) 1) "zset4"

scan 7 type zset
1) "0"
2) null

迭代 hash type

bash 复制代码
scan 0 type hash
1) "7"
2) 1) "hash5"

scan 7 type hash
1) "0"
2) null
2.2.3.3 使用 cursor 和 match 迭代
bash 复制代码
scan 0 MATCH str*
1) "7"
2) 1) "str14"
   2) "str8"
   3) "str12"
   4) "str7"
   5) "str11"
   6) "str1"
   7) "str13"
   8) "str10"
   
scan 7 match str*
1) "0"
2) 1) "str3"
   2) "str6"
   3) "str9"
2.2.3.4 使用 cursor 和 count 迭代
bash 复制代码
scan 0 count 5
1) "14"
2) 1) "str14"
   2) "str8"
   3) "str12"
   4) "str7"
   5) "zset4"
   
scan 14 count 5
1) "7"
2) 1) "str11"
   2) "hash5"
   3) "str1"
   4) "set2"
   5) "str13"
   6) "str10"
   
scan 7 count 5
1) "0"
2) 1) "str3"
   2) "str6"
   3) "str9"
2.2.3.5 使用 cursor、match、count、type 迭代
bash 复制代码
# 先查看一下,确实 11 个 str* 的 key
keys * str*
1)  "str1"
2)  "str12"
3)  "str3"
4)  "str6"
5)  "str9"
6)  "str14"
7)  "str8"
8)  "str7"
9)  "str13"
10) "str10"
11) "str11"

# 然后开始用 scan 迭代
scan 0 match str* count 5 type string
1) "14"
2) 1) "str14"
   2) "str8"
   3) "str12"
   4) "str7"
   
scan 14 match str* count 5 type string
1) "7"
2) 1) "str11"
   2) "str1"
   3) "str13"
   4) "str10"
   
scan 7 match str* count 5 type string
1) "0"
2) 1) "str3"
   2) "str6"
   3) "str9"
   
# 最终 scan 确实也返回了 11 个元素,和 keys* 保持了一致

2.3 命令说明

https://redis.io/commands/scan/

scan、sscan、hscan、zscan 都是同类命令,在这里一同介绍。

由于这些命令允许增量迭代,每次调用只返回少量元素,因此它们可以在生产中使用,而不会像KEYS或SMEMBERS这样的命令那样在对大的键或元素集合调用时会阻塞服务器很长时间(甚至几秒钟)。

然而,虽然像SMEMBERS这样的阻塞命令能够在给定时刻提供属于Set的所有元素,但是SCAN命令家族只提供了对返回元素的有限保证,因为我们递增地遍历的集合在迭代过程中可能会发生变化。

请注意,SCAN、SSCAN、HSCAN和ZSCAN的工作原理非常相似,因此本文档涵盖了所有四个命令。然而,一个明显的区别是,在SSCAN、HSCAN和ZSCAN的情况下,第一个参数是保存集合、哈希或排序集值的键的名称。scan 命令不需要任何键名称参数,因为它迭代当前数据库中的键,因此迭代的对象就是数据库本身。

2.3.1 基本使用

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

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

bash 复制代码
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"

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

如上所示,SCAN返回值是一个由两个值组成的数组:第一个值是下一个调用中使用的新游标,第二个值是一个元素数组。

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

2.3.2 返回值

SCAN、SSCAN、HSCAN和 ZSCAN返回两个元素的多批量应答,其中第一个元素是代表无符号64位数字(游标)的字符串,第二个元素是一个元素数组的多批量应答。

  • SCAN array of elements is a list of keys.
  • SSCAN array of elements is a list of Set members.
  • HSCAN array of elements contains two elements, a field and a value, for every returned element of the Hash.
  • ZSCAN array of elements contains two elements, a member and its associated score, for every returned element of the Sorted Set.

2.3.3 SCAN 的保证和缺点

SCAN命令和SCAN家族中的其他命令能够向用户提供与全迭代相关联的一组保证。

  • 完整的迭代,始终检索从完整迭代开始,到结束时集合中存在的所有元素。这意味着如果一个给定的元素在迭代开始时在集合中,并且在迭代终止时仍然在那里,那么在某个时刻,Scan会将它返回给用户。
  • 完整的迭代,不返回从完整迭代开始,到结束时集合中不存在的任何元素。因此,如果元素在迭代开始之前被移除,并且在迭代持续的所有时间内都不会被添加回集合,扫描将确保该元素永远不会被返回。

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

  • 一个给定的元素可能会多次返回。由应用程序来处理重复元素的情况。例如,只使用返回的元素来执行在多次重新应用时是安全的操作。
  • 在一个完整的迭代过程中,集合中没有经常出现的元素,可以返回也可以不返回:它是未定义的。

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

scan 族函数不能保证每次调用返回的元素数在给定范围内。命令还可以返回零个元素,只要返回的游标不为零,客户端就不应认为迭代已完成。

然而,返回的元素数量是合理的,也就是说,实际上,在迭代大型集合时,扫描可以返回几十个元素的顺序的最大数量的元素,或者当迭代的集合足够小,可以在内部表示为编码的数据结构时,扫描可以在一次调用中返回集合的所有元素(这发生在较小的集合、散列和排序的集合中)。

用户可以 用 COUNT 参数,来调整每次调用返回元素数量的数量级。

2.3.5 COUNT 参数

虽然SCAN并不保证每次迭代返回的元素数量,但是可以使用 COUNT 参数来根据经验调整SCAN的行为。

基本上,使用Count,用户指定每次调用时应该完成的工作量,以便从集合中检索元素。

这只是实现的一个提示,但一般来说,这是您在实现中大多数情况下所期望的。

  • COUNT 的默认值是 10。
  • 当迭代键空间,或一个足够大的集,散列或排序集,如果没有使用MATCH选项,服务器每次调用通常会返回count或多于count元素。(下文会介绍为什么可能返回多于 COUNT 的元素)
  • 当迭代编码为intsets的Sets(仅由整数组成的小集合),或编码为ziplist的散列和排序集(小散列和由小的单个值组成的集合)时,通常所有元素都会在第一次SCAN调用中返回,而不考虑初始值。

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

2.3.6 MATCH 参数

可以只迭代与给定的全局样式模式匹配的元素,这类似于KEYS命令将模式作为其唯一参数的行为。

要做到这一点,只需在扫描命令的末尾附加Match参数(它适用于所有扫描族命令)。

以下是使用Match进行迭代的示例:

bash 复制代码
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"

需要注意,MATCH filter 是后过滤的(即它是在从 collection 中检索元素之后,且将数据返回给客户端之前,操作的)。这意味着,如果 MATCH 与 collection 中很少的元素匹配,扫描很可能在大多数迭代中不返回任何元素。下面是一个示例:

bash 复制代码
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"

如上所示,大多数调用返回零个元素,但最后一个调用使用了1000的前缀,以强制命令为该迭代执行更多的扫描。

当使用 Redis Cluster 时,搜索会针对包含单个插槽的模式进行优化。如果一个模式只能匹配一个插槽中的密钥,那么Redis在搜索匹配模式的密钥时,只会迭代该插槽中的密钥,而不是整个数据库。例如,对于模式 {a}h*llo,Redis只会尝试将其与插槽15495中的密钥进行匹配,这是哈希标签{a}所暗示的。要使用带有散列标记的模式,在 https://redis.io/docs/reference/cluster-spec/#hash-tags 的规范中的散列标记可以查看更多信息。

2.3.7 TYPE 参数

您可以使用TYPE选项要求SCAN只返回与给定类型匹配的对象,从而允许您在数据库中遍历查找特定类型的键。TYPE选项仅适用于整个数据库SCAN,而不适用于 HSCAN 或 ZSCAN 等。

type参数与TYPE命令返回的字符串名称相同。注意一些Redis类型,如GeoHases,HyperLogistics,Bitmaps和Bitfield,可能在内部基于其他Redis类型实现,如基于 string 或 zset,因此SCAN无法将其与相同类型的其他键区分开来。例如,ZSET 和 GEOHASH:

bash 复制代码
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"

2.3.8 支持并发迭代

支持并发迭代。

无限多的客户端可以同时迭代相同的集合,因为迭代器的完整状态在游标中,每次调用时都会获得该游标并将其返回给客户端。根本不采用服务器端状态。

2.3.9 支持在中途终止迭代

由于没有状态服务器端,但是游标捕获了完整的状态,调用者可以自由地中途终止迭代,而无需以任何方式向服务器发送信号。无限次的迭代可以开始,并且永远不会在没有任何问题的情况下终止。

2.3.10 使用损坏的游标调用SCAN

使用中断、否定、超出范围或无效游标调用SCAN,将导致未定义的行为,但不会导致崩溃。没有定义的是,SCAN实现不再能确保返回元素的保证。

可以使用的唯一有效游标是:

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

2.3.11 终止担保

SCAN 算法保证只有在迭代集合的大小保持在给定的最大大小范围内时才会终止,否则迭代总是增长的集合可能会导致SCAN永远不会终止完整的迭代。

这很容易直观地看到:如果集合增长,为了访问所有可能的元素,需要做的工作越来越多,并且终止迭代的能力取决于对SCAN的调用次数和它的 COUNT 参数与集合增长的速率相比。

2.3.12 为什么 scan 可能会在单个调用中返回聚合数据类型的所有项?

在 COUNT 选项文档中,我们指出,有时这类命令可能会在一次调用中同时返回Set、哈希或Sorted Set的所有元素,而不管NTFS选项的值是多少。发生这种情况的原因是,只有当我们扫描的聚合数据类型被表示为哈希表时,基于游标的迭代器才能被实现,并且是有用的。然而Redis使用了内存优化,其中小的聚合数据类型,直到它们达到给定的项目数量或给定的最大单个元素大小,使用紧凑的单分配打包编码来表示。在这种情况下,SCAN没有任何有意义的游标可返回,必须立即对整个数据结构进行初始化,所以它唯一的正常行为就是在调用中返回所有内容。

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

还请注意,这种行为是SSCAN、HSCAN和 ZSCAN特有的。SCAN本身从未显示出这种行为,因为密钥空间总是由哈希表表示。

2.3.13 更多资料

https://redis.io/docs/manual/keyspace

迭代 hash 的示例:

bash 复制代码
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"

2.3.14 更多示例

bash 复制代码
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]

和 Scan 相关的还有如下:

  • SCAN:iterates the set of keys in the currently selected Redis database.
  • SSCAN:iterates elements of Sets types.
  • HSCAN:iterates fields of Hash types and their associated values.
  • ZSCAN:iterates elements of Sorted Set types and their associated scores.

因为 Scan 是迭代式获取数据,不像 KEYS 或 SMEMBERS 可能会使单线程的 Redis 阻塞,所以在生产环境可以放心使用。

参数:因为 Scan 获取的是整个 DB 的各 keys,所以不需要任何参数。

返回值:第一个返回值是 cursor(一个数字),第二个返回值是元素数组。

默认值:默认 COUNT 是 10

bash 复制代码
## 例1: 只写入了 5 个元素,而默认 count 是 10, 则返回 5 个元素
127.0.0.1:6379[5]> MSET k1 1 k2 1 k3 1 k4 1 k5 1
OK
127.0.0.1:6379[5]> KEYS *
1) "k5"
2) "k2"
3) "k1"
4) "k3"
5) "k4"
127.0.0.1:6379[5]> SCAN 0
1) "0"
2) 1) "k5"
   2) "k1"
   3) "k2"
   4) "k3"
   5) "k4"

## 例2: 写入了 23 个元素,而默认 count 是 10, 则分三次迭代可返回所有元素(先 10 个、再 10 个、再 3 个)
127.0.0.1:6379[5]> FLUSHDB
127.0.0.1:6379[5]> MSET k1 1 k2 1 k3 1 k4 1 k5 1 k6 1 k7 1 k8 1 k9 1 k10 1 k11 1 k12 1 k13 1 k14 1 k15 1 k16 1 k17 1 k18 1 k19 1 k20 1 k21 1 k22 1 k23 1
OK
127.0.0.1:6379[5]> KEYS *
 1) "k5"
 2) "k16"
 3) "k17"
 4) "k20"
 5) "k6"
 6) "k3"
 7) "k13"
 8) "k8"
 9) "k4"
10) "k12"
11) "k15"
12) "k10"
13) "k14"
14) "k22"
15) "k19"
16) "k18"
17) "k11"
18) "k7"
19) "k2"
20) "k21"
21) "k9"
22) "k23"
23) "k1"
127.0.0.1:6379[5]> SCAN 0
1) "6" # 因为第一次返回了 cursor=6
2)  1) "k5" # PS: 神奇的没有按默认值返回了 11 条
    2) "k14"
    3) "k20"
    4) "k6"
    5) "k15"
    6) "k23"
    7) "k1"
    8) "k16"
    9) "k12"
   10) "k7"
   11) "k2"
127.0.0.1:6379[5]> SCAN 6 # 所以第二次从 cursor = 6 继续拿
1) "7" # 因为第二次返回了 cursor=7
2)  1) "k8"
    2) "k10"
    3) "k22"
    4) "k19"
    5) "k11"
    6) "k3"
    7) "k13"
    8) "k17"
    9) "k18"
   10) "k21"
   11) "k9"
127.0.0.1:6379[5]> SCAN 7 # 所以第二次从 cursor = 7 继续拿
1) "0" # 因为第三次返回了 cursor=0, 说明拿完了
2) 1) "k4"

# 例3: 手动指定 COUNT
127.0.0.1:6379[5]> FLUSHDB
127.0.0.1:6379[5]> MSET k1 1 k2 1 k3 1 k4 1 k5 1 k6 1 k7 1 k8 1 k9 1 k10 1 k11 1 k12 1 k13 1 k14 1 k15 1 k16 1 k17 1 k18 1 k19 1 k20 1 k21 1 k22 1 k23 1
127.0.0.1:6379[5]> SCAN 0 COUNT 20
1) "11"
2)  1) "k5"
    2) "k14"
    3) "k20"
    4) "k6"
    5) "k15"
    6) "k23"
    7) "k1"
    8) "k16"
    9) "k12"
   10) "k7"
   11) "k2"
   12) "k8"
   13) "k10"
   14) "k22"
   15) "k19"
   16) "k11"
   17) "k3"
   18) "k13"
   19) "k17"
   20) "k18"
127.0.0.1:6379[5]> SCAN 11 COUNT 20
1) "0"
2) 1) "k21"
   2) "k9"
   3) "k4"

# 例4: 手动指定 MATCH 用于模糊匹配
127.0.0.1:6379[5]> FLUSHDB
127.0.0.1:6379[5]> MSET k1 1 k2 1 k3 1 k4 1 k5 1 k6 1 k7 1 k8 1 k9 1 k10 1 k11 1 k12 1 k13 1 k14 1 k15 1 k16 1 k17 1 k18 1 k19 1 k20 1 k21 1 k22 1 k23 1
127.0.0.1:6379[5]> SCAN 0 MATCH k1* COUNT 20
1) "11"
2)  1) "k14"
    2) "k15"
    3) "k1"
    4) "k16"
    5) "k12"
    6) "k10"
    7) "k19"
    8) "k11"
    9) "k13"
   10) "k17"
   11) "k18"
相关推荐
王佑辉7 分钟前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
小码的头发丝、37 分钟前
Django中ListView 和 DetailView类的区别
数据库·python·django
Karoku0661 小时前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix
材料苦逼不会梦到计算机白富美1 小时前
golang分布式缓存项目 Day 1
分布式·缓存·golang
gorgor在码农1 小时前
Redis 热key总结
java·redis·热key
想进大厂的小王1 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
Java 第一深情1 小时前
高性能分布式缓存Redis-数据管理与性能提升之道
redis·分布式·缓存
周全全1 小时前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
白云如幻1 小时前
MySQL的分组函数
数据库·mysql
荒川之神2 小时前
ORACLE 闪回技术简介
数据库·oracle