从今天开始看《Redis深度历险》-- SCAN
场景需求:需要在指定的库里找出符合特定模式的key
首先是keys这个指令,它后面接上一个正则pattern,就能够找出这个实例中所有符合这个pattern的key,但是这个指令有两个缺陷:
1.没有offset和limit参数,一次性返回所有满足条件的key,当符合条件的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 cursor MATCH pattern COUNT num
cursor参数就是游标,pattern是正则表达式,num就是上面特点中的hint
其返回值是一个包含两个元素的列表,第一个元素的是游标值(特点中的第7条),第二个元素是一个列表,列表中的值就是查询结果了。
在redis一篇中已经提到过,redis中的所有key都存储在一个字典中,这个字典的结构和Java中的HashMap一样,都是一个一维数组+二维链表的结构,第一维数组的大小总是2^n (n>=0),扩容一次数组的大小加倍,即n++。scan指令中的COUNT就是一维数组的数量。
scan的遍历顺序非常特别,采用的是高位进位加法来进行遍历而不是从0开始,采用这种方式的原因是为了避免扩容或缩容导致槽位遍历重复或者遗漏。
高位进位加法即从左边开始加,向右边进位,和普通加法正好相反。
redis的字典扩容采用的是渐进式rehash的方式,在基础一篇中我已经写到,在此不再重复。
除了scan外还有sscan、hscan、zscan
scan用于迭代当前数据库种的数据库键
sscan用于迭代集合键中的元素
hscan用于迭代哈希键中的键值对
zscan用于迭代有序集合中的元素(包括元素成员和元素score)
注意,sscan、zscan、hscan命令的第一个参数总是一个数据库键,其返回值的格式与scan一致,而zscan不需要在第一个参数提供任何数据库键,因为它得但的是当前数据库中的所有数据库键。
常见应用--大key扫描,在实际使用的时候可能会由于各种原因导致形成一些很大的key,这些key在数据迁移、扩容或者删除的时候都将会导致服务器卡顿。因此,在实际使用的时候我们就需要尽量避免大key的产生。定位大key就可以对scan查询得到的结果先使用type获取类型,再使用对应的len或者size方法获取其大小,对于扫描结果,将大小前N的显示出来。这可以写一个脚本来处理,但是实际上会比较繁琐,不过在redis广泛的redis-cli中已经提供了这个功能:
作者在这篇中附上的扩展阅读部分是关于美团的一个scan的bug,瞅了一眼,好复杂,感兴趣的可以看看。