Scan扫描

目录

1.scan基础使用

1.1 scan遍历顺序

1.2 更多的scan指令

2.字典扩容

3.大key扫描


在平时线上Redis维护工作中,有时候需要从Redis实例成千上万的key中找到特定的前缀的key列表来手动处理数据,可能是修改它的值,也可能是删除key。redis提供了一个简单粗暴的指令keys用来列出所有满足特定正则字符串规则的key。实现代码如下:

Redis:0>set codehole1 a
OK
Redis:0>set codehole2 b
OK
Redis:0>set codehole3 c
OK
Redis:0>set code1hole a
OK
Redis:0>set code2hole b
OK
Redis:0>set code3hole b
OK
Redis:0>keys codehole*
1) codehole3
2) codehole2
3) codehole1
Redis:0>keys code*hole
1) code3hole
2) code1hole
3) code2hole

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

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

面对这两个显著的缺点,Redis在2.8版本中引入了scan指令。scan相比keys具备以下特点:

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

1.scan基础使用

scan参数提供了三个参数,第一个是cursor整数值,第二个是key的正则模式,第三个是遍历的limit hint。第一次遍历时,cursor值为0,然后将返回结果中第一个整数值作为下一次遍历的cursor。一直遍历到返回的cursor的值为0时结束。执行的实例代码如下:

Redis:0>scan 0 match code* count 10
1) 22
2) code3hole
code1hole
Redis:0>scan 22 match code* count 10
1) 29
2) codehole3
code2hole
codehole2
Redis:0>scan 29 match code* count 10
1) 0    //返回为0,表示遍历已结束
2) codehole1

上面的过程中可以看到,虽然提供的limit是10,但是返回的结果并不是10个。因为这个limit并不是限定返回结果的数量,而是限定服务器单次遍历的字典槽位数量(约定于)。所以,即使返回的结果是空,但游标值不为零,意味着遍历还没有结束。

1.1 scan遍历顺序

scan的遍历顺序非常特别,它不是从第一维数组的第0位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字段的扩容和缩容时避免槽位的遍历重复和泄漏。

高位进位法从左边加,进位往右边移动,同普通加法正好相反。但是最终它们都会遍历所有的槽位,并且没有重复。

1.2 更多的scan指令

scan指令是一系列指令,除了可以遍历所有的key之外,还可以对指定的容器集合进行遍历。比如zsan遍历zset集合元素,hscan遍历hash字典的元素,sscan遍历set集合的元素。它们的原理同scan都是类似的,因为hash底层就是字典,set也是一个特殊的hash(所有的value都是同一个元素)。

2.字典扩容

假设当前的字典的数组长度由8位扩容到16位,那么3号槽位011将会被rehash到3号槽位和11号槽位,也就是说该槽位链表中大约有一半的元素还是3号槽位,其他的元素会放到11号槽位。11这个数字的二进制是1011,就是对3的二进制011增加了一个高位1。

抽象一点说,假设开始槽位的二进制数是XXX,那么该槽位中的元素将被rehash到0XXX和1XXX(XXX + 8)。如果字典长度由16位扩容到32位,那么对于二进制槽位XXXX中的元素将被rehash到0XXXX和1XXXX(XXXX + 16)中。

扩容缩容前后的遍历顺序如下图所示:

假设当前即将遍历110这个位置,那么扩容后,当前槽位上所有的元素对应的新槽位是0110和1110,也就是在槽位的二进制数增加1个高位0或1。这时我们可以直接从0110这个槽位开始往后继续遍历,0110槽位之前的所有槽位都是已经遍历过的,这样可以避免扩容后对已经遍历过的槽位进行重复遍历。

再考虑缩容,假设当前即将遍历110这个位置,缩容后当前槽位所有的元素对应的新槽位是10,也就是去掉二进制最高位。这时,我们可以直接从10这个槽位继续往后遍历,10槽位之前的所有槽位都是已经遍历过的,这样就可以避免缩容的重复遍历。不过缩容还是有一点不同,它会对图中010这个槽位上的元素进行重复遍历,因为缩容后10槽位的元素是010和110上挂接的元素的融合。

3.大key扫描

Redis中如果存储很大的对象,会对Redis的集群数据迁移带来很大问题。如果某个key太大,当需要扩容时会一次性申请更大的一块内存,这也会导致卡顿。如果这个大key被删除,内存会一次性回收,卡顿现象会再一次产生。因此在平时的业务开发中,要尽量避免大key的产生,如果观察到Redis的内存大起大落,这极有可能是因为大key导致的。

为了避免对线上Redis带来卡顿,需要用到scan指令,对于扫描出来的每一个key,使用type指令获得key的类型,然后使用相应数据结构的size或者len来得到它的大小。该过程需要编写脚本,比较繁琐。不过,Redis官方已经在redis-cli指令中提供了这样的扫描功能:

redis-cli -h 127.0.01 -p 7001 --bigkeys

该命令可能会大幅提升Redis的OPS导致线上报警,此时可以增加一个休眠参数。

redis-cli -h 127.0.0.1 -p 7001 --bigkeys -i 0.1

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值