Redis 笔记(13)— scan 和 keys 寻找特定前缀key 字段(命令格式、使用示例、定位大key)

Redis 笔记(13)— scan 和 keys 寻找特定前缀key 字段(命令格式、使用示例、定位大key)

1. keys

Redis 提供了一个简单暴力的指令 keys 用来列出所有满足特定正则字符串规则的 key。

127.0.0.1:6379> keys *

(empty array)

127.0.0.1:6379> set wohu1104go1 1

OK

127.0.0.1:6379> set wohu1104go2 2

OK

127.0.0.1:6379> set wohu1104go3 3

OK

127.0.0.1:6379> set wohu1104go4 5

OK

127.0.0.1:6379> set wohu1104go5 5

OK

127.0.0.1:6379> set wohu1104python 1

OK

127.0.0.1:6379> set wohu1104python2 2

OK

127.0.0.1:6379> set wohu1104python3 3

OK

127.0.0.1:6379> set wohu1104python4 4

OK

127.0.0.1:6379> keys *

1) "wohu1104python4"

2) "wohu1104go3"

3) "wohu1104python3"

4) "wohu1104go1"

5) "wohu1104go4"

6) "wohu1104python2"

7) "wohu1104go2"

8) "wohu1104python"

9) "wohu1104go5"

127.0.0.1:6379> keys wohu1104go*

1) "wohu1104go3"

2) "wohu1104go1"

3) "wohu1104go4"

4) "wohu1104go2"

5) "wohu1104go5"

127.0.0.1:6379> keys wohu1104python*

1) "wohu1104python4"

2) "wohu1104python3"

3) "wohu1104python2"

4) "wohu1104python"

127.0.0.1:6379>

这个指令使用非常简单,提供一个简单的正则字符串即可,但是有很明显的两个缺点。

没有 offset、limit 参数,一次性吐出所有满足条件的 key,万一实例中有几百万个 key 满足条件,输出结果很难分析。keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写 Redis 的其它的指令都会被延后甚至会超时报错,因为 Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才可以继续。

2. scan

2.1 命令

scan命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 scan命令的游标参数, 以此来延续之前的迭代过程。 ​ scan返回一个包含两个元素的数组, 第一个元素是用于进行下一次迭代的新游标, 而第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素。如果新游标返回 0 表示迭代已结束。 ​ scan 指令是一系列指令,除了可以遍历所有的 key 之外,还可以对指定的容器集合进行遍历。比如

zscan 遍历 zset 集合元素hscan 遍历 hash 字典的元素sscan 遍历 set 集合的元素。

它们的原理同 scan 都会类似的,因为 hash 底层就是字典,set 也是一个特殊的 hash(所有的 value 指向同一个元素),zset 内部也使用了字典来存储所有的元素内容。

scan 命令基本语法如下:

SCAN cursor [MATCH pattern] [COUNT count]

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

第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。

2.2 特点

Redis 为了解决这个 keys 性能问题,它在 2.8 版本中加入了指令—— scan。scan 相比 keys 具备有以下特点:

复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程;提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是一个 hint,返回的结果可多可少;同 keys 一样,它也提供模式匹配功能;服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;返回的结果可能会有重复,需要客户端去重,这点非常重要;遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;

2.3 使用

使用下面代码往 Redis 中添加 1000 个有相同前缀的 key

import redis

client = redis.Redis("127.0.0.1", 6379)

for i in range(1000):

client.set("wohu{}".format(i), i)

假设我们要寻找 wohu520 这个 key,那么使用下面的命令,

127.0.0.1:6379> scan 0 match wohu52* count 100

1) "488"

2) 1) "wohu529"

127.0.0.1:6379> scan 488 match wohu52* count 100

1) "372"

2) 1) "wohu52"

127.0.0.1:6379> scan 372 match wohu52* count 100

1) "242"

2) 1) "wohu523"

2) "wohu521"

3) "wohu522"

127.0.0.1:6379> scan 242 match wohu52* count 100

1) "342"

2) (empty array)

127.0.0.1:6379> scan 342 match wohu52* count 100

1) "449"

2) (empty array)

127.0.0.1:6379> scan 449 match wohu52* count 100

1) "261"

2) 1) "wohu527"

127.0.0.1:6379> scan 261 match wohu52* count 100

1) "93"

2) 1) "wohu525"

127.0.0.1:6379> scan 93 match wohu52* count 100

1) "139"

2) 1) "wohu524"

2) "wohu528"

127.0.0.1:6379> scan 139 match wohu52* count 100

1) "279"

2) (empty array)

127.0.0.1:6379> scan 279 match wohu52* count 100

1) "0"

2) 1) "wohu526"

2) "wohu520"

127.0.0.1:6379>

从上面的过程可以看到虽然提供的 limit 是 100,但是返回的结果只有几个甚至没有。因为这个 limit 不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量(约等于)。如果将 limit 设置为 10,你会发现返回结果是空的,但是游标值不为零,意味着遍历还没结束。

2.4 存储结构

在 Redis 中所有的 key 都存储在一个很大的字典中,是一维数组 + 二维链表结构,第一维数组的大小总是 2^n(n>=0),扩容一次数组大小空间加倍,也就是 n++。 ​

scan 指令返回的游标就是第一维数组的位置索引,我们将这个位置索引称为槽 (slot)。如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就行了。limit 参数就表示需要遍历的槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。每一次遍历都会将 limit 数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端。

3. 大 key 定位

有时候会因为业务人员使用不当,在 Redis 实例中会形成很大的对象,比如一个很大的 hash,一个很大的 zset 这都是经常出现的。这样的对象对 Redis 的集群数据迁移带来了很大的问题,在内存分配上,如果一个 key 太大,那么当它需要扩容时,会一次性申请更大的一块内存,这也会导致卡顿。如果这个大 key 被删除,内存会一次性回收,卡顿现象会再一次产生。 ​ 如果观察到 Redis 的内存大起大落,这极有可能是因为大 key 导致的,这时候你就需要定位出具体是那个 key,进一步定位出具体的业务来源,然后再改进相关业务代码设计。 ​ 为了避免对线上 Redis 带来卡顿,这就要用到 scan 指令,对于扫描出来的每一个 key,使用 type 指令获得 key 的类型,然后使用相应数据结构的 size 或者 len 方法来得到它的大小,对于每一种类型,保留大小的前 N 名作为扫描结果展示出来。 ​ 上面这样的过程需要编写脚本,比较繁琐,不过 Redis 官方已经在 redis-cli 指令中提供了这样的扫描功能,我们可以直接拿来即用。

redis-cli -h 127.0.0.1 -p 6379 –-bigkeys

如果你担心这个指令会大幅抬升 Redis 的 ops 导致线上报警,还可以增加一个休眠参数。

redis-cli -h 127.0.0.1 -p 6379 –-bigkeys -i 0.1

上面这个指令每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,但是扫描的时间会变长。 ​

参考: https://juejin.cn/book/6844733724618129422/section/6844733724710404110​

相关推荐