想要返回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到的大表全部位置所有遍歷一遍,而後再返回遍歷元素和下一小表遍歷位置