小河捞虾--scan


面试题:假如 Redis 里面有 1 亿个key,其中有 10w 个key 是以 某个固定的已知的前缀开头的,如果将它们全部找出来?

答:使用 keys 指令可以扫出指定模式的 key 列表。
对方接着追问: 如果这个 redis 正在给线上的业务提供服务, 那使用 keys 指令会有什么 问题? 这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞 一段时间, 线上服务会停顿, 直到指令执行完毕, 服务才能恢复。

这个时候可以使用 scan 指令, scan 指令可以无阻塞的提取出指定模式的 key 列表, 但是会有一定的重复概率, 在客户端做一次去重就可以了, 但是整体所花费的时间会比直接用 keys 指令长。

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

keys ?

keys 有两个缺点:

  1. 查询复杂度 O(n),如果实例中有千万级以上的 key ,这个指令就会导致 Redis 服务卡顿,所有读写 Redis 的其他指令都会被延后甚至会超时报错,因为 Redis 是单线程程序,顺序执行所有指令,其他指令必须等到当前的 keys指令执行完了才可以继续。
  2. 没有 offset,limit 参数,一次性吐出所有满足条件的key,刷屏没有尽头,可用性不高。

scan 特点:

  1. 复杂度虽然也是O(n)时,但它是通过游标分步进行的,不会阻塞线程

  2. 提供 limit 参数,可以控制每次返回结果的最大条数, limit 只是个 hint,返回的结果可多可少。

  3. keys 一样,它也提供模式匹配功能。

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

  5. 返回的结果可能会有重复,需要客户端去重,这点非常重要。

  6. 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的。

  7. 单次返回的结果是空的并不意昧着遍历结束,而要看返回的游标值是否为零。

一、scan基本用法

scan cursor [match] [count]

scan 46440 match 333* count 100 # 46440 游标整数值,333* key的正则,100 limit hint;

第一次遍历游标整数值为0,每次遍历返回一个整数,如果不为零,可以继续遍历直到为零,遍历完成。

请添加图片描述

二、字典结构

​ Redis 中所有的 key 都存储在一个很大的字典中,这个字典的结构和 Java 中的HashMap 一样,如下图所示,它是一维数组,是二维链表结构。第一维数组的大小总是2n(n>=O ),扩容一次数组,大小空间加倍,也就是2n+1.

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

请添加图片描述

三、scan遍历顺序和缩容、扩容

遍历

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

请添加图片描述

扩容

Java 中的 HashMap 有扩容的概念,当 LoadFactor 达到闰值时,需要重新分配一个新的 倍大小的数组,然后将所有的元素全部 rehash 挂到新的数组下面。

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

请添加图片描述

假设现在遍历到110 这个位置(橙色位置)

扩容:rehash后,当前槽位上所有的元素对应的新槽位为 0110 ,1110;遍历从0110继续,前面的槽位已经遍历了,所以不会重复遍历

缩容:rehash后,当前槽位上所有的元素对应新槽位为10; 遍历从10 继续,不过会重复遍历现槽位010的元素,不可避免。

四、渐进式rehash

Java HashMap 在扩容时会一次性将旧数组下挂接的元素全部转移到新数组。如 HashMap 中元素特别多,线程就会出现卡顿现象

Redis 为了解决这个问题,采用“渐进式 rehash ,,。它会同时保留旧数组和新数组,然后在定时任务中以及后续对 hash 的指令操作申渐渐地将旧数组中挂攘的元素迁移到新数组上。这意昧着要操作处于 rehash 中的字典,需要同时访问新旧两个数组结构。如果在旧数组下面找不到元素,还需要去新数组下面寻找。scan 也需要考虑这个问题,对于 rehash 中的字典, 它需要同时扫描新旧槽位然后将结果融合后返回给客户端。

五、其他数据结构的scan指令

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

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

六、大key扫描

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

!!平时使用redis,应避免大key

如何定位到大key?

Redis 官方已经在 redis-cli 指令中提供了这样的扫描功能,我们可以直接拿来使用。

re di 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 指令就会休眠 O.ls, ops 就不会剧烈抬升,但是扫描的时间会变长。

参考书籍:《Redis深度历险 核心原理与应用实践》_钱文品

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值