Redis在2.8.0版本新增了眾望所歸的scan操作,從此再也不用擔心敲入了keys*, 然后舉起雙手看着鍵盤等待漫長的系統卡死了···
命令的官方介紹在這里, 中文版由huangz同學細心翻譯了,作者Antirez的介紹在這里:Finally Redis collections are iterable (我又邪惡的想到了之前他那次機器down機的事故了···)。
具體的使用參考上面的鏈接即可,這里大概介紹一下Scan操作的實現原理。
Redis的SCAN操作由於其整體的數據設計,無法提供特別准的scan操作,僅僅是一個“can ‘ t guarantee , just do my best”的實現,優缺點如下:
- 優點:
- 提供鍵空間的遍歷操作,支持游標,復雜度O(1), 整體遍歷一遍只需要O(N);
- 提供結果模式匹配;
- 支持一次返回的數據條數設置,但僅僅是個hints,有時候返回的會多;
- 弱狀態,所有狀態只需要客戶端需要維護一個游標;
- 缺點:
- 無法提供完整的快照遍歷,也就是中間如果有數據修改,可能有些涉及改動的數據遍歷不到;
- 每次返回的數據條數不一定,極度依賴內部實現;
- 返回的數據可能有重復,應用層必須能夠處理重入邏輯;
所以結論是Scan是一個不錯的但也讓人又愛又恨的命令···。下面來介紹一下代碼。
首先scanCommand 函數處理簡單的scan操作,其他類似hscan函數跟這個的區別就是hscan需要取獲取一遍key對應的空間或者說域,他們主要都是嚼用了通用的scan操作函數:scanGenericCommand 。
scanGenericCommand 函數分4步:
第一步當然就是解析參數了,比如count, match匹配參數;
第二部是需要去做真正的掃描鍵 的操作了,redis為了性能考慮,對於小數據結構會轉換為ziplist,intset數據結構因此需要區分這2類,對於后者,由於其本身比較小,因此可完全可以在這一次scan操作的時候返還所有的數據,反正不大的。
另外一類就是正常的hash表所代表的掃描了,其掃描路徑比較復雜,好吧,我看了好幾次都沒有看明白這到底是怎么掃描的,這幾天啃也要啃出來!
/* Handle the case of a hash table. */
ht = NULL;
if (o == NULL) {//鍵掃描
ht = c->db->dict;
} else if (o->type == REDIS_SET && o->encoding == REDIS_ENCODING_HT) {
ht = o->ptr;
} else if (o->type == REDIS_HASH && o->encoding == REDIS_ENCODING_HT) {
ht = o->ptr;
count *= 2; /* We return key / value for this type. */
} else if (o->type == REDIS_ZSET && o->encoding == REDIS_ENCODING_SKIPLIST) {
zset *zs = o->ptr;
ht = zs->dict;
count *= 2; /* We return key / value for this type. */
}
//由於redis的ziplist, intset等類型數據量挺少,所以可用一次返回的。下面的else if 做這個事情。全部返回一個key 。
if (ht) {//一般的存儲,不是intset, ziplist
void *privdata[2];
/* We pass two pointers to the callback: the list to which it will
* add new elements, and the object containing the dictionary so that
* it is possible to fetch more data in a type-dependent way. */
privdata[0] = keys;
privdata[1] = o;
do {
//一個個掃描,從cursor開始,然后調用回調函數將數據設置到keys返回數據集里面。
cursor = dictScan(ht, cursor, scanCallback, privdata);
} while (cursor && listLength(keys) < count); } else if (o->type == REDIS_SET) {
int pos = 0;
int64_t ll;
while(intsetGet(o->ptr,pos++,&ll))//將這個set里面的數據全部返回,因為它是壓縮的intset,會很小的。
listAddNodeTail(keys,createStringObjectFromLongLong(ll));
cursor = 0;
} else if (o->type == REDIS_HASH || o->type == REDIS_ZSET) {//那么一定是ziplist了,字符串表示的數據結構,不會太大。
unsigned char *p = ziplistIndex(o->ptr,0);
unsigned char *vstr;
unsigned int vlen;
long long vll;
while(p) {//掃描整個鍵,然后全部返回這一條。並且返回cursor為0表示沒東西了。其實這個就等於沒有遍歷
ziplistGet(p,&vstr,&vlen,&vll);
listAddNodeTail(keys,
(vstr != NULL) ? createStringObject((char*)vstr,vlen) : createStringObjectFromLongLong(vll));
p = ziplistNext(o->ptr,p);
}
cursor = 0;
} else {
redisPanic("Not handled encoding in SCAN.");
}
上面簡單的地方在於如果這個鍵是已REDIS_SET或者REDIS_HASH或者REDIS_ZSET行事存儲的話,那么只需要掃描所有的鍵,然后一個個將其加入到臨時的列表里面,以備返回給客戶端。
最難的地方在於dictScan 函數,里面是各種位運算。
隨后第三步就是進行結果的過濾了,一般就是用match參數代表的字符串去做匹配,看是否需要過濾數據。
第四步就是將收集到的數據返回給客戶端。然后就完成了請求。
dictScan 原理:
好吧,我看了2次,沒看懂·····先做飯··
ps: 寫着寫着發現一篇文章寫不完,所以令起一篇了:Redis Scan迭代器遍歷操作原理(二)–dictScan反向二進制迭代器 , 希望能講清楚.
