想要返回redis当前数据库中的全部key应该怎么办?用keys命令?在key很是多的状况下,该命令会致使单线程redis服务器执行时间过长,后续命令得不到响应,同时对内存也会形成必定的压力,严重下降redis服务的可用性html
为此redis 2.8.0及以上版本提供了多个scan相关命令,用以针对不一样数据结构(如数据库、集合、哈希、有序集合)提供相关遍历功能c++
SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements)redis
0
时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0
的游标时, 表示迭代已结束注:以上内容摘自http://redisdoc.com/key/scan.html算法
对于SCAN命令和底层采用了哈希表实现的集合、哈希、有序集合,遍历时采用了一样的scan算法(都会调用dictScan函数),dictScan函数短小精悍,正是本文尝试解释的核心,以下数据库
unsigned long dictScan(dict *d,//待遍历哈希表 unsigned long v,//cursor值,这次遍历位置,初始为0 dictScanFunction *fn,//单个条目遍历函数,根据条目类型,copy条目对象,以便加入到返回对象中 dictScanBucketFunction* bucketfn,//null void *privdata)//返回对象 {
dictht *t0, *t1;
const dictEntry *de, *next;
unsigned long m0, m1;
if (dictSize(d) == 0) return 0;//若是dict为空,直接返回
if (!dictIsRehashing(d)) {//若是此刻哈希表没有在rehashing,只有ht[0]有数据
t0 = &(d->ht[0]);//将ht[0]做为遍历表
m0 = t0->sizemask;//遍历表的sizemask,即以遍历表的size为底取模,如表大小为8,则m0为111
/* 遍历cursor所在位置的全部条目 */
if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);//这行if条件为false,不会执行
de = t0->table[v & m0];
while (de) {//遍历当前cursor位置的全部条目,即hash key取模hash table大小相同的全部条目
next = de->next;
fn(privdata, de);
de = next;
}
/* 做用是将v也就是cursor的高位置为1,低位不变,如v为001,则改成61个1再加001 */
v |= ~m0;
/* 将cursor高位0变成1或者(连续高位1都变成0且第一个0变为1) */
v = rev(v);//将cursor作二进制逆序,也就是变成100+61个1
v++;//末位加1,也就是101+61个0
v = rev(v);//将cursor作二进制逆序,也就是61个0+101
} else {//哈希表正在rehashing
t0 = &d->ht[0];
t1 = &d->ht[1];
/* 确保t0小t1大 */
if (t0->size > t1->size) {
t0 = &d->ht[1];
t1 = &d->ht[0];
}
m0 = t0->sizemask;//t0表sizemask,如表大小为8,则m0为7,即0111
m1 = t1->sizemask;//t1表sizemask,如表大小为64,则m1为63,即00111111
/* 将cursor位置的全部条目都添加进去 */
if (bucketfn) bucketfn(privdata, &t0->table[v & m0]);//不执行
de = t0->table[v & m0];
while (de) {//将t0的全部条目都加进去
next = de->next;
fn(privdata, de);
de = next;
}
/* 遍历小表cursor位置可能会rehash到大表的全部条目, *如cursor为1,小表大小为8,大表大小为64,则0、八、1六、2四、3二、40、4八、56等位置的条目都会被添加返回 */
do {
/* 添加大表v位置的全部元素,注意v位置跟着while循环不断变化 */
if (bucketfn) bucketfn(privdata, &t1->table[v & m1]);//不执行
de = t1->table[v & m1];
while (de) {//添加v位置的全部条目
next = de->next;
fn(privdata, de);
de = next;
}
/* 做用同上,只不过换成了大表的元素,也就是小表cursor位置可能扩展到大表的全部位置*/
v |= ~m1;
v = rev(v);
v++;
v = rev(v);
/* 如上举例,m0为3位1,m1为6位1,两者作异或,也就是将两者不一样的高位置为1, *其余先后的61位均为0,而后遍历v在两者不一样高位的全部可能, *当v从新回到0时,跳出while循环 ,也就是将m0可能rehash到的m1位置的条目所有返回 */
} while (v & (m0 ^ m1));
}
return v;
}
复制代码
这个算法很是精妙,看了挺久才明白点意思,若有不当之处,欢迎拍砖服务器
哈希表有多种状态,遍历时有可能处于微信
这就使得scan算法面临的状况很复杂,怎样遍历完全部元素(遍历过程当中没有发生变化的元素保证遍历完)且尽量少的返回重复元素是个难题 三种状态具体的遍历流程图示推演发个传送门:Redis Scan迭代器遍历操做原理(二)–dictScan反向二进制迭代器 (网上搜的,流程很长,慎点,可是有一些不错的图)数据结构
具体算法流程也可参见上面的源码注释函数
一、假设hash表大小从N扩张为2^M x N(哈希表大小只可能为2的幂数,N也为2的幂数),那么原先hash表的i元素可能被分布到i + j x N where j <- [0, 2^M-1]位置,如N为4,M为3,则i(原先为1)可能被分散到一、1+1x四、1+2x4...、1+7x4位置,注意这些位置,它们后面的log(N)位是相同的,也就是前面的M位不一样,如spa
若是在扩展后遍历的过程当中能将后面两位相同都为01的位置都忽略,也就是只要后面N位相同的遍历完了,意味着前面M位的全部可能性也都列举完了,即老是先把前面的可能性穷举完,再穷举后面的位,那么扩展后的slot(如1对应的一、五、9...、29)就没必要从新再从新遍历一遍了,收缩是相似的,只不过收缩后的位置可能包含原哈希表高位还没有穷举完的可能性,须要再次遍历
二、怎么先遍历高位的可能性,dictScan给出了反向二进制迭代算法(老是先将最高位置取反,穷举高位的可能性,依次向低位推动,这种变换方式确保了全部元素都会被遍历到):
连续1
置为0,第一个0置为1,如10001下一个是01001,即开始穷举下一个高位的可能性三、rehashing这种状况,须要在遍历完小表cursor位置后将小表cursor位置可能rehash到的大表全部位置所有遍历一遍,而后再返回遍历元素和下一小表遍历位置