在平时线上Redis 维护工作中,有时候需要从Redis 实例的成千上万个key中找 出特定前缀的key 列表来手动处理数据,可能是修改它的值,也可能是删除key 。这 里就有个问题, 如何从海量的key 中找出满足特定前缀的key 列表? Redis 提供了一个简单粗暴的指令keys 用来列出所有满足特定正则字符串规则 的key 。只要提供一个简单的正则字符串即可。
127.0.0.1:6379> set codeholel 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> keys *
1) "codehole2"
2) "codehole3"
3) "books"
4) "codeholel"
5) "foo"
127.0.0.1:6379> set code1hol a
OK
127.0.0.1:6379> set code2hol b
OK
127.0.0.1:6379> keys code*hol
1) "code1hol"
2) "code2hol"
缺点
- 没有offset、limit 参数,一次性吐出所有满足条件的key
- keys 算法是遍历算法,复杂度是0(n),如果实例中有千万级以上的key ,这个指令就会导致Redis 服务卡顿,所有读写Redis 的其他指令都会被延后甚至会超时报错,因为Redis 是单线程程序,顺序执行所有指令,其他指令必须等到当前的keys指令执行完了才可以继续。(注:6.0后引入了多线程,有关介特性可参考zhuanlan.zhihu.com/p/144805500…
在满足需求和存在造成Redis卡顿之间究竟要如何选择呢?面对这个两难的抉择,Redis在2.8版本给我们提供了解决办法——scan命令。 相比于keys命令,scan命令有两个比较明显的优势:
- scan命令的时间复杂度虽然也是O(N),但它是分次进行的(利用游标特性,在数据集中一条一条的获取),不会阻塞线程。
- scan命令提供了limit参数,可以控制每次返回结果的最大条数。 scan 参数提供了三个参数,第一个是 cursor 整数值,第二个是 key 的正则模式,第三个是遍历的 limit hint。 第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。
127.0.0.1:6379> scan 0 match code* count 100
1) "0"
2) 1) "codehole2"
2) "codeholel3"
3) "codeholel"
4) "code1hol"
5) "code2holel3"
6) "codeholel1"
7) "codehole3"
8) "code3holel3"
9) "code2hol"
10) "code1holel3"
11) "codeholel2"
遍历到游标值为0时结束。
字典结构 scan命令就是对这个一维数组进行遍历。每次返回的游标值也都是这个数组的索引。limit参数表示遍历多少个数组的元素,将这些元素下挂接的符合条件的结果都返回。因为每个元素下挂接的链表大小不同,所以每次返回的结果数量也就不同。以下为具体示例:
遍历顺序
127.0.0.1:6379> keys *
1) "db_number"
2) "key1"
3) "myKey"
127.0.0.1:6379> scan 0 MATCH * COUNT 1
1) "2"
2) 1) "db_number"
127.0.0.1:6379> scan 2 MATCH * COUNT 1
1) "1"
2) 1) "myKey"
127.0.0.1:6379> scan 1 MATCH * COUNT 1
1) "3"
2) 1) "key1"
127.0.0.1:6379> scan 3 MATCH * COUNT 1
1) "0"
2) (empty list or set)
scan的遍历顺序是
0->2->1->3
这个顺序看起来有些奇怪。我们把它转换成二进制就好理解一些了。 00->10->01->11
原来挂接在xx下的所有元素被分配到0xx和1xx下。在上图中,当我们即将遍历10时,dict进行了rehash,这时,scan命令会从010开始遍历,而000和100(原00下挂接的元素)不会再被重复遍历。
我们会发现采用高位进位加法的遍历顺序, rehash 后的槽位在遍历顺序上是相邻的。
再来看看缩容的情况。假设dict从3位缩容到2位,当即将遍历110时,dict发生了缩容,这时scan会遍历10。这时010下挂接的元素会被重复遍历,但010之前的元素都不会被重复遍历了。所以,缩容时还是可能会有些重复元素出现的。
这个序列是高位加1的。普通二进制的加法,是从右往左相加、进位。而这个序列是从左往右相加、进位的
从图中可以看出高位进位加法从左边力日,进位往右边移动,同普通加法正好相反。
但是最终它们都会遍历所有的槽位并且没有重复。
Java 的HashMap 在扩容时会一次性将旧数组下挂接的元素全部转移到新数组下面。如果HashMap 中元素特别多,线程就会出现卡顿现象。Redis 为了解决这个问题,采用渐进式rehash。它会同时保留旧数组和新数组,然后在定时任务中以及后续对hash 的指令操作申渐渐地将旧数组中挂攘的元素迁移到新数组上。这意昧着要操作处于rehash 中的 字典,需要同时访问新旧两个数组结构。如果在旧数组下面找不到元素,还需要去新数组下面寻找。 scan 也需要考虑这个问题,对于rehash 中的字典,它需要同时扫描新旧槽位,然后将结果融合后返回给客户端。
大key扫描
如果某个key 太大,会导致数据迁移卡顿。另外在内存分配上,如果个key 太大,那么当它需要扩容时,会一次性申 请更大的一块内存,这也会导致卡顿。如果这个大key 被删除,内存会被-次性回收。在平时的业务开发中,要尽量避兔大key 的产生。Redis 官方已经在redis-cli 指令中提供了这样的扫描功能,我们可以直接拿来使用。
./redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.1
可以添加参数i,上面这个指令每隔100 条scan 指令就会休眠0.1s, ops 就不会剧烈抬升,但是扫描的时间会变长。