SCAN cursor [MATCH pattern] [COUNT count] SSCAN KEY cursor [MATCH pattern] [COUNT count] HSCAN KEY cursor [MATCH pattern] [COUNT count] ZSCAN KEY cursor [MATCH pattern] [COUNT count]
scan:迭代当前库redis
sscan:迭代一个 set 类型数组
hscan:迭代一个hash类型,并返回相应的值服务器
zscan:迭代一个sorted set,而且返回相应的分数函数
redis是单进程单线程模型,keys和smembers这种命令可能会阻塞服务器,因此出现了scan系列的命令,经过返回一个游标,能够增量式迭代.性能
scan,sscan,hscan,zsan分别有本身的命令入口,入口中会进行参数检测和游标赋值,而后进入统一的入口函数:scanGenericCommand,以hscan命令为例:spa
scanGenericCommand主要分四步:线程
游标在保存为hash的时候发挥做用,具体入口函数为dictScan,下文详细描述。设计
当迭代一个哈希表时,存在三种状况:code
redis中进行rehash时会存在两个哈希表,ht[0]与ht[1],而且是渐进式rehash(即不会一次性所有rehash);新的键值对会存放到ht[1]中而且会逐步将ht[0]的数据转移到ht[1].所有rehash完毕后,ht[1]赋值给ht[0]而后清空ht[1].blog
所以游标的实现须要兼顾以上三种状况,以上三种状况的游标实现要求以下:
假设bucket0读完以后返回了游标1,当客户端再次带着游标1返回时哈希表已经进行完rehash,而且size扩大了一倍变成了8.redis按以下方法计算一个键的bucket:
hash(key)&(size-1)
即若是size是4时,hash(key)&11,若是size是8时,hash(key)&111.所以当从4扩容到8时,原先在0bucket的数据会分散到0(000)与4(100)两个bucket,bucket对应关系表以下:
从二进制来看,当size为4时,hash(key)以后取低两位即 hash(key)&11即key的bucket位置,若是size为8时,bucket位置为 hash(key)&111,即取低三位,当低两位为00时,若是第三位为0,则为000,若是第三位为1,则为100,正好是4.其余槽位的相似.因此若是此时继续按第一种方法遍历,第四个bucket取到的值所有为重复值
因此为了兼顾以上三种状况,作到不漏数据而且尽可能不重复,redis使用了一种叫作reverse binary iteration的方法.具体的游标计算代码以下:
代码逻辑很简单,下面示例从4变为8和从4变为16以及从8变为4和从16变为4时,这种方法为什么可以作到不重不漏
遍历size为4时的游标状态转移为0-2-1-3.
同理,size为8时的游标状态转移为0-4-2-6-1-5-3-7.
size为16时的游标状态转义为0-8-4-12-2-10-6-14-1-9-5-13-3-11-7-15
能够看出,当size由小变大时,全部原来的游标都能在大的hashTable中找到相应的位置,而且顺序一致,不会重复读取而且不会遗漏
例如size原来是4变为了8,且第二次遍历时rehash已经完成.此时游标为2,根据图2,咱们知道size为4时的bucket2会rehash到size为8时的2和6.而size为4时的bucket0rehash到size为8时的0和4
因为bucket 0 已经遍历完,也即size为8时的0,4已经遍历,正好开始从2开始继续遍历,不重复也不会遗漏
继续考虑size由大变小的状况.假设size由16变为了4,分两种状况,一种是游标为0,2,1,3中的一种,此时继续读取,也不会遗漏和重复
但若是游标返回的不是这四种,例如返回了10,10&11以后变为了2,因此会从2开始继续遍历.但因为size为16时的bucket2已经读取过,而且2,10,6,14都会rehash到size为4的bucket2,因此会形成重复读取
size为16时的bucket2。即有重复但不会遗漏
总结一下:redis里边rehash从小到大时,scan系列命令不会重复也不会遗漏.而从大到小时,有可能会形成重复但不会遗漏.
截止目前,状况1和状况2已经比较完美的处理了。状况3看看如何处理
状况3须要从ht[0]和ht[1]中都取出数据,主要的难点在于如何在size大的哈希表中找到应该取哪些bucket.redis代码以下:
判断条件为:
v&(m0^m1)
size 4的m0为00000011,size8的m1为00000111,两者异或以后取值为00000100,即取两者mask高位的值,而后&v,看游标是否在高位还有值
下一个游标的取值方法为
v = ( ((v | m0) +1)& ~m0) | ( v & m0)
右半部分 取v的低位,左半部分取v的高位。 (v&m0)取出v的低位 例如size = 4时为 v&00000011
左半部分 (v|m0) + 1即将v的低位都置为1,而后+1以后会进位到v的高位,再次 & ~m0以后即取出了v的高位
总体来看每次将游标v的高位加1.下边举例来看:
假设游标返回了2,而且正在进行rehash,此时size由4变成了8 .则m0 = 00000011 v = 00000010
根据公式计算出的下一个游标为 ( (( 00000010|00000011) +1 ) & (11111100) )| (00000010 & 00000011) = (00000100)&(11111100)|(00000010) = (00000110) 正好是6
判断条件为 (00000010) & (00000011 ^ 00000111) = (00000010) & (00000100) = (00000000) 为0,结束循环