Redis 的 Scan

此文档源自钱文品老师所著《Redis 深度历险:核心原理和应用实践》

在平时线上 Redis 维护工作中,有时候需要从 Redis 实例成千上万的 key 中找出特定前缀的 key 列表来手动处理数据,可能是修改它的值,也可能是删除 key。这里就有一个问题,如何从海量的 key 中找出满足特定前缀的 key 列表来?

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

127.0.0.1:6379> set codehole1 a
OK
127.0.0.1:6379> set codehole2 b
OK
127.0.0.1:6379> set codehole3 c
OK
127.0.0.1:6379> set code1hole a
OK
127.0.0.1:6379> set code2hole b
OK
127.0.0.1:6379> set code3hole b
OK
127.0.0.1:6379> keys *
1) "codehole1"
2) "code3hole"
3) "codehole3"
4) "code2hole"
5) "codehole2"
6) "code1hole"
127.0.0.1:6379> keys codehole*
1) "codehole1"
2) "codehole3"
3) "codehole2"
127.0.0.1:6379> keys code*hole
1) "code3hole"
2) "code2hole"
3) "code1hole"

这个指令使用非常简单,提供一个简单的正则字符串即可,但是有很明显的两个缺点。
1、没有offset、limit 参数,一次性吐出所有满足条件的 key,万一实例中有几百 w 个key 满足条件,当你看到满屏的字符串刷的没有尽头时,你就知道难受了。
2、keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写 Redis 的其它的指令都会被延后甚至会超时报错,因为Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才可以继续。

面对这两个显著的缺点该怎么办呢?
Redis 为了解决这个问题,它在 2.8 版本中加入了大海捞针的指令——scan。scan 相比
keys 具备有以下特点:
1、复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程;
2、提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是一个 hint,返回的结果可多可少;
3、同 keys 一样,它也提供模式匹配功能;
4、服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;
5、返回的结果可能会有重复,需要客户端去重复,这点非常重要;
6、遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
7、单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;

scan 基础使用

在使用之前,让我们往 Redis 里插入 10000 条数据来进行测试

import redis
client = redis.StrictRedis()
for i in range(10000):
 client.set("key%d" % i, i)

好,Redis 中现在有了 10000 条数据,接下来我们找出以 key99 开头 key 列表。
scan 参数提供了三个参数,第一个是 cursor 整数值,第二个是 key 的正则模式,第三个是遍历的 limit hint。第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。

127.0.0.1:6379> scan 0 match key99* count 1000
1) "13976"
2) 1) "key9911"
 2) "key9974"
 3) "key9994"
 4) "key9910"
 5) "key9907"
 6) "key9989"
 7) "key9971"
 8) "key99"
 9) "key9966"
 10) "key992"
 11) "key9903"
 12) "key9905"
127.0.0.1:6379> scan 13976 match key99* count 1000
1) "1996"
2) 1) "key9982"
 2) "key9997"
 3) "key9963"
 4) "key996"
 5) "key9912"
 6) "key9999"
 7) "key9921"
 8) "key994"
 9) "key9956"
 10) "key9919"
127.0.0.1:6379> scan 1996 match key99* count 1000
1) "12594"
2) 1) "key9939"
 2) "key9941"
 3) "key9967"
 4) "key9938"
 5) "key9906"
 6) "key999"
 7) "key9909"
 8) "key9933"
 9) "key9992"
......
127.0.0.1:6379> scan 11687 match key99* count 1000
1) "0"
2) 1) "key9969"
 2) "key998"
 3) "key9986"
 4) "key9968"
 5) "key9965"
 6) "key9990"
 7) "key9915"
 8) "key9928"
 9) "key9908"
 10) "key9929"
 11) "key9944"

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

127.0.0.1:6379> scan 0 match key99* count 10
1) "3072"
2) (empty list or set)

字典的结构

在 Redis 中所有的 key 都存储在一个很大的字典中,这个字典的结构和 Java 中的HashMap 一样,是一维数组 + 二维链表结构,第一维数组的大小总是 2^n(n>=0),扩容一次数组大小空间加倍,也就是 n++。
在这里插入图片描述
scan 指令返回的游标就是第一维数组的位置索引,我们将这个位置索引称为槽 (slot)。如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就行了。limit 参数就表示需要遍历的槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。每一次遍历都会将 limit 数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端。

scan 遍历顺序

scan 的遍历顺序非常特别。它不是从第一维数组的第 0 位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。普通加法和高位进位加法的区别高位进位法从左边加,进位往右边移动,同普通加法正好相反。但是最终它们都会遍历所有的槽位并且没有重复。

更多的 scan 指令

scan 指令是一系列指令,除了可以遍历所有的 key 之外,还可以对指定的容器集合进行遍历。比如 zscan 遍历 zset 集合元素,hscan 遍历 hash 字典的元素、sscan 遍历 set 集合的元素。

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

大 key 扫描

有时候会因为业务人员使用不当,在 Redis 实例中会形成很大的对象,比如一个很大的hash,一个很大的 zset 这都是经常出现的。这样的对象对 Redis 的集群数据迁移带来了很大的问题,因为在集群环境下,如果某个 key 太大,会数据导致迁移卡顿。另外在内存分配上,如果一个 key 太大,那么当它需要扩容时,会一次性申请更大的一块内存,这也会导致卡顿。如果这个大 key 被删除,内存会一次性回收,卡顿现象会再一次产生。

在平时的业务开发中,要尽量避免大 key 的产生。

如果你观察到 Redis 的内存大起大落,这极有可能是因为大 key 导致的,这时候你就需要定位出具体是那个 key,进一步定位出具体的业务来源,然后再改进相关业务代码设计。

那如何定位大 key 呢?

为了避免对线上 Redis 带来卡顿,这就要用到 scan 指令,对于扫描出来的每一个key,使用 type 指令获得 key 的类型,然后使用相应数据结构的 size 或者 len 方法来得到它的大小,对于每一种类型,保留大小的前 N 名作为扫描结果展示出来。

上面这样的过程需要编写脚本,比较繁琐,不过 Redis 官方已经在 redis-cli 指令中提供了这样的扫描功能,我们可以直接拿来即用。
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys
如果你担心这个指令会大幅抬升 Redis 的 ops 导致线上报警,还可以增加一个休眠参数。
redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1
上面这个指令每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,但是扫描的时间会变长。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Redis Scan 是 Redis 中的一个命令,用于迭代获取数据库中的键。它的作用类似于其他数据库中的 SELECT 命令。 你可以使用 Redis Scan 命令来获取特定类型的键、匹配特定模式的键、按照字典序排序的键等。 使用方法: ``` SCAN cursor [MATCH pattern] [COUNT count] ``` 参数说明: - cursor:游标,用于记录扫描进度。 - pattern:可选参数,用于匹配键的模式。 - count:可选参数,指定每次迭代返回的键的数量。 例如,你可以使用以下命令来获取所有以 "user:" 开头的键: ``` SCAN 0 MATCH user:* ``` 注意,Redis Scan 命令不能保证返回的键是按照字典序排序的,因此在使用时需要注意。 ### 回答2: Redis的SCAN命令用于迭代数据库中的key集合,它通过遍历数据库来获取匹配指定模式的全部key。由于Redis是单线程操作的,如果在一个请求中执行SCAN命令时,数据库发生了对key的增删操作,可能会导致某些key被遍历不到或者重复遍历,为了解决这个问题,可以使用游标(cursor)参数进行分段遍历。 SCAN命令的基本使用格式为:SCAN cursor [MATCH pattern] [COUNT count]。其中,cursor代表游标的起始位置,初始值为0;MATCH参数用于指定要匹配的key的模式,默认为不进行匹配;COUNT参数用于指定每次遍历返回的元素个数,默认值为10。 执行SCAN命令后,Redis会返回一个包含两部分信息的列表,第一部分是当前游标的值,用于下一次迭代时作为参数传入SCAN命令中;第二部分是匹配模式下的key列表。 通过连续执行SCAN命令,并将上一次命令返回的游标值作为下一次命令的参数,可以逐步遍历整个数据库中的所有key。但需要注意的是,由于SCAN命令是基于迭代器实现的,所以可能会有一些key被遗漏或重复遍历的情况发生。 使用SCAN命令可以避免在业务中使用KEYS命令带来的性能问题。在大数据量场景下,KEYS命令会导致Redis阻塞,直到遍历完全部key集合。而使用SCAN命令可以通过分段遍历的方式,减少了阻塞时间,可以更好地保证Redis的性能和稳定性。 总之,SCAN命令是一种高效且可靠的遍历Redis数据库中key的方法,通过游标参数的配合,可以有效地获取匹配指定模式的全部key。 ### 回答3: Redis Scan 是 Redis 提供的一个命令,用于遍历数据库中的所有键。 在 Redis 中,键值对是以键值对方式存储的,而键是用来唯一标识一个值的字符串。Redis Scan 命令可以遍历数据库中的所有键,用于查找满足指定模式的键。它的语法是: ``` scan cursor [MATCH pattern] [COUNT count] ``` 其中,`cursor` 是一个游标,表示遍历的起始位置,一般可以设置为0以开始遍历;`MATCH pattern` 是一个可选参数,用于指定匹配的模式,只返回匹配模式的键;`COUNT count` 是一个可选参数,用于指定每次遍历返回的键的数量,默认值是10。 使用 Redis Scan 可以灵活地遍历数据库中的键值对,而无需事先知道具体的键的名称。它可以用于实现一些功能,比如获取指定前缀的所有键值对、分页查询等。 需要注意的是,Redis Scan 是一个迭代器,它通过游标来遍历数据库,每次返回一部分键。当返回结果中的游标值为0时,表示遍历完成。为了获取所有的键,需要多次调用 Redis Scan 命令,直到返回结果中的游标值为0为止。 使用 Redis Scan 是一个高效的遍历数据库的方式,可以减少对服务器的压力,特别是在数据量较大时。但由于它是一个近似值的遍历方式,返回结果中可能包含一些已经被删除的键,需要在客户端进行二次过滤。 总之,Redis Scan 是一个非常有用的命令,可以灵活地遍历数据库中的键值对,提供了方便的方法来查找满足指定模式的键。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值