应用 9:大海捞针 —— Scan

1.keys

在Redis维护工作中,有时需要从Redis实例成千上万个key值找出特定前缀的key列表,如何在海量的key值中找到满足特定前缀的key列表?

keys用来列出满足特定正则字符串规则的key值

(1)缺点

a.没有offset、limit参数,一次性输出所有满足条件的key值,这样对于满足条件较多的情况会造成很大的困扰。

b.且使用遍历算法,复杂度O(n),如果满足条件的key较多,这个指令就会造成Redis服务卡顿,所有其他读写指令都会被延后甚至超时报错,因为Redis是单线程,会顺序执行所有指令。

2.scan

(1)与keys比较

a.复杂度还是O(n),但是通过游标分步进行的,不会阻塞线程

b.提供limit,控制最大返回结果数量

c.提供模式匹配功能

d.服务器不需要为游标保存状态,游标唯一的状态就是scan返回给客户端的游标整数

e.返回结果可能有重复,需要客户端去重

f.遍历过程数据可能被修改,改动的数据能否被遍历是不确定的

g.单次返回的结果是空的并不意味遍历结束,而应该根据返回的游标值是否为零

3.scan的使用

(1)三个参数:cursor整数值;key的正则模式;遍历的limit

第一次遍历时,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"
......

limit为1000,返回的值却只有10个左右。limit不是限定返回的结果数量,而是限定服务器单次遍历字典槽位数量。如果限定为10,返回结果是空的,但不意味着遍历结束,游标值还不为0。

4.字典的结构

一维数组+二维链表结构,一维数组的大小总是2^n(n>=0),扩容一次数组大小空间加倍。

scan 指令返回的游标就是第一维数组的位置索引,我们将这个位置索引称为槽 (slot)。

如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就行了。limit 参数就表示需要遍历的

槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽

位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。每一次遍历都会将 limit

数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端。

5.了解高位进位加法

简单理解就是左边加,进位往右边移动

以四位举例:

0000+1=1000
1000+1=0100
0100+1=1100
1100+1=0010
0010+1=1010
1010+1=0110
0110+1=1110
1110+1=0001
....

 6.字典扩容

当字典达到一定阙值,需要重新分配一个两倍的数组,然后将所有元素重新rehash到新的数组下面。rehash就是hash值对数组长度进行取模运算。长度变量,所有每个元素的挂接的槽位可能也变了。又因为数组长度是2^n次方,所有取模运算等价于

a mod 8 = a & (8-1) = a & 7
a mod 16 = a & (16-1) = a & 15
a mod 32 = a & (32-1) = a & 31

这里的 7, 15, 31 称之为字典的 mask 值,mask 的作用就是保留 hash 值的低位,高位都被设置为 0。

rehash前后元素槽位变化

假设当前的字典的数组长度由 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) 中。

7.scan遍历顺序

采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容

时避免槽位的遍历重复和遗漏。

对比扩容缩容前后的遍历顺序

使用高位进位加法的遍历顺序,rehash后的槽位在遍历顺序上是相邻的。

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

对于缩容,假设即将遍历 110 这个位置 (橙色),那么缩容后,当前槽位所有的

元素对应的新槽位是 10(深绿色),也就是去掉槽位二进制最高位。这时我们可以直接从 10

这个槽位继续往后遍历,10 槽位之前的所有槽位都是已经遍历过的,这样就可以避免缩容的

重复遍历。不过缩容还是不太一样,它会对图中 010 这个槽位上的元素进行重复遍历,因为

缩融后 10 槽位的元素是 010 和 110 上挂接的元素的融合。

8.渐进式rehash

Java 的 HashMap 在扩容时会一次性将旧数组下挂接的元素全部转移到新数组下面。如果 HashMap 中元素特别多,线程就会出现卡顿现象。Redis 为了解决这个问题,它采用渐进式 rehash。它会同时保留旧数组和新数组,然后在定时任务中以及后续对 hash 的指令操作中渐渐地旧数组中挂接的元素迁移到新数组上。这意味着要操作处于 rehash 中的字典,需要同时访问新旧两个数组结构。如果在旧数组下面找不到元素,还需要去新数组下面去寻找。scan 也需要考虑这个问题,对与 rehash 中的字典,它需要同时扫描新旧槽位,然后将结果融合后返回给客户端。

9.scan指令

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

10.关于大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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值