大海捞针——scan

如何从海量的 key 中找出满足特定前缀的 key 列表?

Redis提供了一个命令用来列出所有满足特定正则字符串规则的key。

keys * #查询所有key keys codehole* #查询codehole后缀的所有key

keys code*hole #查询code和hole夹在中间的所有key

有两个很明显的缺点

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

在 2.8版本中引入了scan,与keys相比:

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

scan 基本用法

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

scan cursor match pattern count limit

字典的结构

它是一维数 组,是二维链表结构。第一维数组的大小总是 2的n次方 (n>=0 ),扩容一次数组,大小空间加倍, 也就是 2的n次方加1。 scan 指令返回的游标就是第一维数组的位置索引,我们将这个位置索引称为槽( slot )。

scan 遍历顺序

采用了高位进位加法来遍历,是为了考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。高位进位加法从左边加,进位往右边移动,同普通加法正好相反。

字典扩容

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

渐进式 rehash

它会同时保留旧数组和新数组,然后在定时任务中以及后续对 hash 的指令操作中渐渐地将旧数组中挂接的元素迁移到新数组上。

更多的 scan 指令

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

大 key 扫描

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

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

redis cli - h 127.0.0.1 - p port -bigkeys

为了避免会大幅抬升 Redis的ops 导致线上报警,可以使用redis cli - h 127.0.0.1 - p port -bigkeys -i 0.1 令每隔 100条 scan 指令就会休眠 0.ls