redis专题10 海量数据扫描神器之scan
  热度 °
用redis
模糊匹配key
时,官方建议不要使用keys
或smembers
,他们的时间复杂度都是O(N)
,使用scan
,zscan
,hscan
等。scan
系列增量式迭代命令每次执行的复杂度为O(1)
, 对数据集进行一次完整迭代的复杂度为O(N)
, 其中N
为数据集中的元素数量。相比keys
命令执行时会阻塞掉整个redis
线程而言,scan
系列则是通过游标分步进行的,不会阻塞redis
线程, 且在同一时间,可以有任意多个客户端对同一数据集进行迭代。
keys
有两个显著缺点,
- 没有
offset, limit
参数,一次性吐出所有满足条件的key
, 万一实例中有几百w个key
满足条件, 当你看到满屏的字符串刷的没有尽头时,你就知道难受了。 keys
算法是遍历算法, 复杂度是O(n)
,如果实例中有千万级以上的key
,这个指令就会导致Redis
服务卡顿, 所有读写Redis
的其它的指令都会被延后甚至会超时报错,因为Redis
是单线程程序,顺序执行所有指令,其它指令必须等到当前的keys
指令执行完了才可以继续。
redis
为了解决这个问题, 它在2.8版本中加入了scan
系列增量式迭代命令。scan
相比keys
具备有以下特点:
- 复杂度虽然也是
O(n)
,但是它是通过游标分步进行的,不会阻塞线程; - 提供
limit
参数,可以控制每次返回结果的最大条数,limit
只是一个hint
,返回的结果可多可少; - 同
keys
一样,它也提供模式匹配功能; - 服务器不需要为游标保存状态,游标的唯一状态就是
scan
返回给客户端的游标整数; - 返回的结果可能会有重复,需要客户端去重复,这点非常重要;
- 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
- 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;
当然
增量式迭代命令
也不是没有缺点, 如使用SMEMBERS
命令可以返回集合键当前包含的所有元素, 但是对于SCAN
这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中,键可能会被修改,所以增量式迭代命令只能对被返回的元素提供有限的保证(offer limited guarantees about the returned elements)。
同一个元素可能会被返回多次。
scan
系列命令无法保证同一元素被多次返回, 所以处理重复元素的工作交由应用程序负责处理(过滤/幂等操作)。
scan
命令:
SCAN cursor [MATCH pattern] [COUNT count]
count
表示每次迭代中应该从数据集里最多返回多少元素, 默认值为10, 用户可以在每次迭代时随机修改此值;
在迭代一个足够大的、由哈希表实现的数据库、集合键、哈希键或者有序集合键时, 如果用户没有使用
match
选项, 那么命令返回的元素数量通常和COUNT
选项指定的一样, 或者比COUNT
选项指定的数量稍多一些。
在迭代一个编码为整数集合(
intset
, 一个只由整数值构成的小集合), 或者编码为压缩列表ziplist
, 由不同值构成的一个小哈希或者一个小有序集合)时, 增量式迭代命令通常会无视COUNT
选项指定的值, 在第一次迭代就将数据集包含的所有元素都返回给用户。
match
表示对返回的元素再进行模式匹配并将匹配结果最终返回, 需要注意的是, 对元素的模式匹配工作是在命令从数据集中取出元素之后, 向客户端返回元素之前的这段时间内进行的, 所以如果被迭代的数据集中只有少量元素和模式相匹配, 那么迭代命令或许会在多次执行中都不返回任何元素。
功能: 用于迭代当前数据库中的数据库键;
返回值:
SCAN
命令的回复是一个包含两个元素的数组, 第一个数组元素是用于进行下一次迭代的新游标, 而第二个数组元素则是一个数组, 这个数组中包含了所有被迭代的元素。当返回的新游标为0 表示当前迭代全部完成;
1 | 127.0.0.1:6379> keys key* |
字典结构
在redis
中所有的key
都存储在一个很大的字典中, 即一维数组 + 二维链表结构, 示例,1
2
3
4
5
60
1 ->e->f->h
2
3 ->g->x
4
5 ->a->cscan
指令返回的游标就是第一维数组的位置索引,我们将这个位置索引称为槽(slot)
。如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就行了。count
参数就表示需要遍历的槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。每一次遍历都会将count
数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端。
scan
的遍历顺序非常特别。它不是从第一维数组的第0
位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。
redis字典扩容采用了·渐进式rehash`操作。
渐进式rehash
会同时保留旧数组和新数组,然后在定时任务中以及后续对hash
的指令操作中渐渐地将旧数组中挂接的元素迁移到新数组上。这意味着要操作处于rehash
中的字典,需要同时访问新旧两个数组结构。如果在旧数组下面找不到元素,还需要去新数组下面去寻找。
scan
也需要考虑这个问题,对与rehash
中的字典,它需要同时扫描新旧槽位,然后将结果融合后返回给客户端。
scan
系列命令目前共计4条,
SCAN
命令用于迭代当前数据库中的数据库键;SSCAN
命令用于迭代集合键中的元素;HSCAN
命令用于迭代哈希键中的键值对;ZSCAN
命令用于迭代有序集合中的元素(包括元素成员和元素分值);
定位大key
为了避免对线上redis
带来卡顿,这就要用到scan
指令,对于扫描出来的每一个key
,使用type
指令获得key
的类型,然后使用相应数据结构的size
或者len
方法来得到它的大小,对于每一种类型,保留大小的前N
名作为扫描结果展示出来。
上面这样的过程redis
官方已经在redis-cli
指令中提供了这样的扫描功能,我们可以直接拿来即用。1
redis-cli -h 127.0.0.1 -p 6370 –-bigkeys
如果你担心这个指令会大幅抬升redis
的ops
导致线上报警,还可以增加一个休眠参数。1
redis-cli -h 127.0.0.1 -p 6370 –-bigkeys -i 0.1
上面这个指令每隔100
条scan
指令就会休眠0.1s,
ops`就不会剧烈抬升,但是扫描的时间会变长。
可进一步参考 美团针对Redis Rehash机制的探索和实践
总结
- 从
redis
模糊匹配场景分析入手,引入keys
和scan
系列增量迭代式命令方法,结合使用场景对其优劣进行了比较说明; - 阐述了
scan
遍历方法,并对字典扩容进行了简单探讨; - 从定位大
key
场景出发,引出了redis-cli
已支持的--bigkeys
命令, 可以非常方便的协助用户定位大key
问题;
作者署名:朴实的一线攻城狮
本文标题:redis专题10 海量数据扫描神器之scan
本文出处:http://researchlab.github.io/2018/10/07/redis-10-scan/
版权声明:本文由Lee Hong创作和发表,采用署名(BY)-非商业性使用(NC)-相同方式共享(SA)国际许可协议进行许可,转载请注明作者及出处, 否则保留追究法律责任的权利。