前言
Redis中有一个经典的问题,在巨大的数据量的情况下,做类似于查找符合某种规则的Key的信息,这里就有两种方式,
一、 keys命令
简单粗暴,由于Redis单线程这一特性,keys命令是以阻塞的方式执行的,keys是以遍历的方式实现的复杂度是 O(n),Redis库中的key越多,查找实现代价越大,产生的阻塞时间越长。
keys * 、keys codehole* 分别是查询全部的key以及查询前缀为codehole的key。特点太暴力,性能不好,搜索的是整个redis;
缺点:
1、没有 offset、limit 参数,一次性吐出所有满足条件的 key,万一实例中有几百 w 个 key 满足条件,当你看到满屏的字符串刷的没有尽头时,你就知道难受了。
2、keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写 Redis 的其它的指令都会被延后甚至会超时报错,因为 Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才可以继续。
二、 scan命令
以非阻塞的方式实现key值的查找,绝大多数情况下是可以替代keys命令的,可选性更强
scan命令的特点:
1、复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程;
2、提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是一个 hint,返回的结果可多可少;
3、同 keys 一样,它也提供模式匹配功能;
4、服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;
5、返回的结果可能会有重复,需要客户端去重复,这点非常重要;
正常情况下,使用scan没问题,如果正在rehash,则会造成重读
比如现在有四个桶,读了0,1,发生rehash时, 0会到4上,1是到5,造成重读,
redis使用高位递增遍历,如下, 当02,遍历完以后0426肯定是遍历完的
但缩容有可能发成key重复
00 0
10 2
01 1
11 3
000 0
100 4
010 2
110 6
001 1
101 7
011 3
111 8
6、遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
7、单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零
三、keys、scan命令具体用法
比如这里查询key111开头的key有哪些?
若使用keys命令,则执行keys key1111*,一次性全部查出来。

同样,如果使用scan命令,则用scan 0 match key1111* count 20

scan的语法为:SCAN cursor [MATCH pattern] [COUNT count]The default COUNT value is 10.
SCAN命令是一个基于游标的迭代器。这意味着命令每次被调用都需要使用上一次这个调用返回的游标作为该次调用的游标参数,以此来延续之前的迭代过程。
这里使用scan 0 match key1111* count 20命令来完成这个查询,稍显意外的是,使用一开始都没有查询到结果,这个要从scan命令的原理来看。
scan在遍历key的时候,0就代表第一次,key1111*代表按照key1111开头的模式匹配,count 20中的20并不是代表输出符合条件的key,而是限定服务器单次遍历的字典槽位数量(约等于)。
那么,什么又叫做槽的数据?这个槽是不是Redis集群中的slot?答案是否定的。其实上图已经给出了答案了。
如果上面说的“字典槽”的数量是集群中的slot,又知道集群中的slot数量是16384,那么遍历16384个槽之后,必然能遍历出来所有的key信息,上面清楚地看到,当遍历的字典槽的数量20000的时候,游标依旧没有走完遍历结果,因此这个字典槽并不等于集群中的slot的概念。
经过测试,在scan的时候,究竟遍历多大的COUNT值能完全match到符合条件的key,跟具体对象的key的个数有关,
如果以超过key个数的count来scan,必定会一次性就查找到所有符合条件的key,比如在key个数为10W个的情况下,一次遍历20w个字典槽,肯定能完全遍历出来结果。

四、探究scan底层源码
Redis的结构
Redis使用了Hash表作为底层实现,原因不外乎高效且实现简单。说到Hash表,很多Java程序员第一反应就是HashMap。没错,Redis底层key的存储结构就是类似于HashMap那样数组+链表的结构。其中第一维的数组大小为2n(n>=0)。每次扩容数组长度扩大一倍。
scan命令就是对这个一维数组进行遍历。每次返回的游标值也都是这个数组的索引。limit参数表示遍历多少个数组的元素,将这些元素下挂接的符合条件的结果都返回。因为每个元素下挂接的链表大小不同,所以每次返回的结果数量也就不同。
SCAN的遍历顺序
关于scan命令的遍历顺序,我们可以用一个小栗子来具体看一下。
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)

本文深入解析Redis中的SCAN命令,对比KEYS命令,介绍SCAN的非阻塞性质、游标机制及模式匹配功能。探讨SCAN命令在rehash过程中的表现,以及高位递增遍历策略。通过源码分析,理解SCAN遍历顺序和Redis rehash机制。
最低0.47元/天 解锁文章
1483

被折叠的 条评论
为什么被折叠?



