在平时线上 Redis 维护⼯作中,有时候需要从 Redis 实例成千上万的 key 中找出特定前缀的 key 列表来⼿动处理数据,可能是修改它的值,也可能是删除 key。这⾥就有⼀个问题,如何从海量的 key中找出满⾜特定前缀的 key 列表来?Redis 提供了⼀个简单暴⼒的指令 keys ⽤来列出所有满⾜特定正则字符串规则的 key。
127.0.0.1:6379> set codehole1 a
OK
127.0.0.1:6379> set codehole2 b
OK
127.0.0.1:6379> set codehole3 c
OK
127.0.0.1:6379> set code1hole a
OK
127.0.0.1:6379> set code2hole b
OK
127.0.0.1:6379> set code3hole b
OK
127.0.0.1:6379> keys *
1) "codehole1"
2) "code3hole"
3) "codehole3"
4) "code2hole"
5) "codehole2"
6) "code1hole"
127.0.0.1:6379> keys codehole*
1) "codehole1"
2) "codehole3"
3) "codehole2"
127.0.0.1:6379> keys code*hole
1) "code3hole"
2) "code2hole"
3) "code1hole"

这个指令使⽤⾮常简单,提供⼀个简单的正则字符串即可,但是有很明显的两个缺点。
1. 没有 offset、limit 参数,⼀次性吐出所有满⾜条件的 key,万⼀实例中有⼏百 w 个 key 满⾜条件,当你看到满屏的字符串刷的没有尽头时,你就知道难受了。
2. keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写Redis 的其它的指令都会被延后甚⾄会超时报错,因为 Redis是单线程程序,顺序执⾏所有指令,其它指令必须等到当前的keys 指令执⾏完了才可以继续。
⾯对这两个显著的缺点该怎么办呢?
Redis 为了解决这个问题,它在 2.8 版本中加⼊了⼤海捞针的指令——scan。scan 相⽐ keys 具备有以下特点:
1. 复杂度虽然也是 O(n),但是它是通过游标分步进⾏的,不会阻塞线程;
2. 提供 limit 参数,可以控制每次返回结果的最⼤条数,limit 只是⼀个 hint,返回的结果可多可少;
3. 同 keys ⼀样,它也提供模式匹配功能;
4. 服务器不需要为游标保存状态,游标的唯⼀状态就是 scan 返回给客户端的游标整数;
5. 返回的结果可能会有重复,需要客户端去重复,这点⾮常重要;
6. 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
7. 单次返回的结果是空的并不意味着遍历结束,⽽要看返回的游标值是否为零;

scan 基础使⽤

在使⽤之前,让我们往 Redis ⾥插⼊ 10000 条数据来进⾏测试import redis
client = redis.StrictRedis()
for i in range(10000):
client.set("key%d" % i, i)
好,Redis 中现在有了 10000 条数据,接下来我们找出以 key99
开头 key 列表。
scan 参数提供了三个参数,第⼀个是 cursor 整数值,第⼆个是
key 的正则模式,第三个是遍历的 limit hint。第⼀次遍历时,
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"
......
127.0.0.1:6379> scan 11687 match key99* count
1000
1) "0"
2) 1) "key9969"
2) "key998"
3) "key9986"
4) "key9968"
5) "key9965"
6) "key9990"
7) "key9915"
8) "key9928"
9) "key9908"
10) "key9929"11) "key9944"
从上⾯的过程可以看到虽然提供的 limit 是 1000,但是返回的结果只有 10 个左右。因为这个 limit 不是限定返回结果的数量,⽽是限定服务器单次遍历的字典槽位数量(约等于)。如果将 limit 设置为10,你会发现返回结果是空的,但是游标值不为零,意味着遍历还没结束。
127.0.0.1:6379> scan 0 match key99* count 10
1) "3072"
2) (empty list or set)
字典的结构
在 Redis 中所有的 key 都存储在⼀个很⼤的字典中,这个字典的结构和 Java 中的 HashMap ⼀样,是⼀维数组 + ⼆维链表结构,第⼀维数组的⼤⼩总是 2^n(n>=0),扩容⼀次数组⼤⼩空间加倍,也就是 n++。scan 指令返回的游标就是第⼀维数组的位置索引,我们将这个位置索引称为槽 (slot)。如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就⾏了。limit 参数就表示需要遍历的槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。每⼀次遍历都会将 limit 数量的槽位上挂接的所有链表元素进⾏模式匹配过滤后,⼀次性返回给客户端。
scan 遍历顺序
scan 的遍历顺序⾮常特别。它不是从第⼀维数组的第 0 位⼀直遍历到末尾,⽽是采⽤了⾼位进位加法来遍历。之所以使⽤这样特殊的⽅式进⾏遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。
字典扩容
Java 中的 HashMap 有扩容的概念,当 loadFactor 达到阈值时,需要重新分配⼀个新的 2 倍⼤⼩的数组,然后将所有的元素全部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) 中。对⽐扩容缩容前后的遍历顺序观察这张图,我们发现采⽤⾼位进位加法的遍历顺序,rehash 后的槽位在遍历顺序上是相邻的。假设当前要即将遍历 110 这个位置 (橙⾊),那么扩容后,当前槽位上所有的元素对应的新槽位是 0110 和 1110(深绿⾊),也就是在槽位的⼆进制数增加⼀个⾼位 0 或 1。这时我们可以直接从 0110 这个槽位开始往后继续遍历,0110 槽位之前的所有槽位都是已经遍历过的,这样就可以避免扩容后对已经遍历过的槽位进⾏重复遍历。再考虑缩容,假设当前即将遍历 110 这个位置 (橙⾊),那么缩容后,当前槽位所有的元素对应的新槽位是 10(深绿⾊),也就是去掉槽位⼆进制最⾼位。这时我们可以直接从 10 这个槽位继续往后遍历,10 槽位之前的所有槽位都是已经遍历过的,这样就可以避免缩容的重复遍历。不过缩容还是不太⼀样,它会对图中 010 这个槽位上的元素进⾏重复遍历,因为缩融后 10 槽位的元素是 010 和 110上挂接的元素的融合。

渐进式 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 被删除,内存会⼀次性回收,卡顿现象会再⼀次产⽣。在平时的业务开发中,要尽量避免⼤ 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 就不会剧烈抬升,但是扫描的时间会变⻓。
扩展阅读
感兴趣可以继续深⼊阅读 美团近期修复的Scan的⼀个bug(https://mp.weixin.qq.com/s/ufoLJiXE0wU4Bc7ZbE9cDQ)

转载于:https://www.cnblogs.com/hanmengya/p/10967540.html

⼤海捞针 —— Scan相关推荐

  1. 500个Python模块(库)的详细分类介绍

    常用模块 Chardet-------------字符编码探测器,可以自动检测文本.网页.xml的编码.colorama------------主要用来给文本添加各种颜色,并且非常简单易用.Prett ...

  2. shopping更任性,不止有淘宝,还有智慧商场!

    为什么爱逛街的你,现在基本只上淘宝,从前拿着实物都要货比三家,现在只看哪个淘宝模特更好看.是逛街太累还是其他什么原因呢?其实,你只是不了解"智慧商场",任性方便的shopping, ...

  3. 百度搜索引擎的工作原理 鏀惰棌鍒帮細 时间:2015-07-10 文章来源:马海祥博客 访问次数:4330 关于百度以及其它搜索引擎的工作原理,其实大家已经讨论过很多,但随着科技的进步、互联网

    关于百度以及其它搜索引擎的工作原理,其实大家已经讨论过很多,但随着科技的进步.互联网业的发展,各家搜索引擎都发生着巨大的变化,并且这些变化都是飞快的,本文的目的,除了从百度官方的角度发出一些声音.纠正 ...

  4. 摆脱海王式电销,外呼系统提升销售效率业绩

    传统电销模式相对较为粗犷,采用"海王式"的方法去海捞客户,这种模式效率较低下不说,还存在一个严重的问题就是容易导致手机号卡被停用的情况,如果持续使用这种模式企业销售经营将被拖垮. ...

  5. 32年寻获上帝粒子,华人女学者自述高能人生故事

    她出身低微,却从小下定决心:经济上绝不依赖男人:她拿到硕士学位的那天,却因为女性身份被保安赶出哈佛庭院:她留学美国,九年没有见到家人,邀请父亲参加博士毕业典礼时,却发现父亲已在一年前去世:她为了学术道 ...

  6. Indexes and Indexing

    许多因素决定了 MySQL 的性能,但索引是最为特殊的,因为没有它们就无法实现性能.您可以删除其他因素(查询[query].模式[schema].数据[data]等)并仍然获得性能,但删除索引会将性能 ...

  7. “天鹅”类谜解大全!(转载)

    发信人: tana (tana), 信区: Riddle 标  题: "天鹅"类谜解大全!(转载) 发信站: 鼓浪听涛 (Thu Nov  6 20:19:29 2003) 1:天 ...

  8. 李宏毅2021机器学习笔记——General Guidance

    General Guidance : overfit Framework of ML ​ 我们已经看了作业一了,其实之后好几个作业,它看起来的样子,基本上都是大同小异 ​ 就是你会有一堆训练的资料,这 ...

  9. 深度学习21_李宏毅_03_General Guidance

    General Guidance : overfit Framework of ML ​ 我们已经看了作业一了,其实之后好几个作业,它看起来的样子,基本上都是大同小异 ​ 就是你会有一堆训练的资料,这 ...

最新文章

  1. 每日一皮:没有好好测试就运行,还自信的不得了...
  2. Codeforces Round #343 (Div. 2) D. Babaei and Birthday Cake 线段树维护dp
  3. appium第一个安卓自动化工程
  4. java图形界面猜字游戏,java程序,猜字游戏,希望大神帮忙
  5. python with contextmanager yield 语法糖
  6. 【ABAP】报表进度提示
  7. 公司间采购的后台配置备忘录
  8. 韩信点兵python源代码_少儿编程|Python小课堂 – 韩信点兵
  9. mysql 存储 事务_MYSQL 可以在存储过程里实现事务控制吗
  10. ajax提交file空指针,excel导入上传文件报空指针错误
  11. java integer reverse_Leetcode7 Reverse Integer Java实现及分析
  12. 【maven】maven的介绍
  13. 一次有趣的面试经历,当前端面试碰到后端面试官会发生什么?
  14. jsp数据库连接大全和数据库操作封装到Javabean
  15. 输出星期名缩写python_python练习题5.1输出星期名缩写
  16. C#批量提取DXF文件中的尺寸,公差和形位公差
  17. 彩色图像处理之色彩学基础
  18. 【游戏行业观察】篇1:成龙与《传奇》:传统网游营销模式的变迁
  19. 完全卸载Android Studio的方法
  20. setup界面的network configuration 进不去的原因

热门文章

  1. php分页循环生成htnl,PHP分页类,生成分页html字符串
  2. 1的阶乘在c语言里咋表示,C语言编程求阶乘1到10并分别显示在屏幕上 – 手机爱问...
  3. java炸弹游戏_java实现数字炸弹
  4. 传奇手游服务器搭建_热血传奇3月开服计划
  5. 数据分析数据拼接案例
  6. (13) 悲观锁和乐观锁解决hibernate并发(转)
  7. MyListUtil.java list工具类
  8. JAVAEE框架之Spring注解
  9. mysql安装8.013_Mysql 8.0.13 安装
  10. 配置Apache Basic和Digest认证