大海捞针——scan
如何从海量的 key 中找出满足特定前缀的 key 列表?
Redis提供了一个命令用来列出所有满足特定正则字符串规则的key。
keys *
#查询所有key keys codehole*
#查询codehole后缀的所有key
keys code*hole
#查询code和hole夹在中间的所有key
有两个很明显的缺点
- 没有 offset、 limit 参数,一次性吐出所有满足条件的 key ,如果实例中有非常多 key 满足条件,很难找出你需要的有效数据。
- keys 算法是遍历算法,复杂度是 O(n) ,如果实例中有千万级以上的 key ,这个指令就会导致 Redis 服务卡顿,所有读写 Redis 的其他指令都会被延后甚至会超时报错,因为 Redis 是单线程程序,顺序执行所有指令,其他指令必须等到当前的 keys 指令执行完了才可以继续。
在 2.8版本中引入了scan,与keys相比:
- 复杂度虽然也是 O(n),但它是通过游标分步进行的,不会阻塞线程
- 提供 limit 参数,可以控制每次返回结果的最大条数, limit 只是个 hint, 返回的结果可多可少。
- 同keys 一样,它也提供模式匹配功能。
- 服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数。
- 返回的结果可能会有重复,需要客户端去重,这点非常重要。
- 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的。
- 单次返回的结果是空的并不意昧着遍历结束,而要看返回的游标值是否为零。
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