Grapephp
增量迭代一個集合元素。node
SCAN cursor [MATCH pattern] [COUNT count]
redis 127.0.0.1:6379> scan 0 1) "17" 2) 1) "key:12" 2) "key:8" 3) "key:4" 4) "key:14" 5) "key:16" 6) "key:17" 7) "key:15" 8) "key:10" 9) "key:3" 10) "key:7" 11) "key:1" redis 127.0.0.1:6379> scan 17 1) "0" 2) 1) "key:5" 2) "key:18" 3) "key:0" 4) "key:2" 5) "key:19" 6) "key:13" 7) "key:6" 8) "key:9" 9) "key:11"
對於增量式迭代命令不保證每次迭代所返回的元素數量,咱們可使用COUNT選項, 對命令的行爲進行必定程度上的調整。COUNT 選項的做用就是讓用戶告知迭代命令, 在每次迭代中應該從數據集裏返回多少元素。使用COUNT 選項對於對增量式迭代命令至關於一種提示, 大多數狀況下這種提示都比較有效的控制了返回值的數量。git
COUNT 參數的默認值爲 10 。
數據集比較大時,若是沒有使用MATCH 選項, 那麼命令返回的元素數量一般和 COUNT 選項指定的同樣, 或者比 COUNT 選項指定的數量稍多一些。
在迭代一個編碼爲整數集合(intset,一個只由整數值構成的小集合)、 或者編碼爲壓縮列表(ziplist,由不一樣值構成的一個小哈希或者一個小有序集合)時, 增量式迭代命令一般會無視 COUNT 選項指定的值, 在第一次迭代就將數據集包含的全部元素都返回給用戶。
注意: 並不是每次迭代都要使用相同的 COUNT 值 ,用戶能夠在每次迭代中按本身的須要隨意改變 COUNT 值, 只要記得將上次迭代返回的遊標用到下次迭代裏面就能夠了。github
127.0.0.1:6379> scan 0 count 2 1) "12" 2) 1) "user_level_1" 2) "mykey" 127.0.0.1:6379>
相似於KEYS 命令,增量式迭代命令經過給定 MATCH 參數的方式實現了經過提供一個 glob 風格的模式參數, 讓命令只返回和給定模式相匹配的元素。redis
redis 127.0.0.1:6379> sadd myset 1 2 3 foo foobar feelsgood (integer) 6 redis 127.0.0.1:6379> sscan myset 0 match f* 1) "0" 2) 1) "foo" 2) "feelsgood" 3) "foobar" redis 127.0.0.1:6379>
SCAN, SSCAN, HSCAN 和 ZSCAN 命令都返回一個包含兩個元素的 multi-bulk
回覆: 回覆的第一個元素是字符串表示的無符號 64 位整數(遊標),回覆的第二個元素是另外一個 multi-bulk 回覆, 包含了本次被迭代的元素。segmentfault
此篇以scan命令爲例。數組
/* The SCAN command completely relies on scanGenericCommand. */ void scanCommand(client *c) { unsigned long cursor; if (parseScanCursorOrReply(c,c->argv[1],&cursor) == C_ERR) return; scanGenericCommand(c,NULL,cursor); }
/* 嘗試解析存儲在對象「o」中的掃描遊標:若是遊標有效, * 則將其做爲無符號整數存儲到*cursor中,並返回C_OK。不然返回C_ERR並向客戶機發送錯誤。 * 此處o->ptr存儲咱們輸入的遊標 */ int parseScanCursorOrReply(client *c, robj *o, unsigned long *cursor) { char *eptr; /* 使用strtoul(),由於咱們須要一個無符號long, * 因此getLongLongFromObject()不會覆蓋整個遊標空間。 */ errno = 0; *cursor = strtoul(o->ptr, &eptr, 10); if (isspace(((char*)o->ptr)[0]) || eptr[0] != '\0' || errno == ERANGE) { addReplyError(c, "invalid cursor"); return C_ERR; } return C_OK; }
void scanGenericCommand(client *c, robj *o, unsigned long cursor) { int i, j; list *keys = listCreate(); listNode *node, *nextnode; long count = 10; sds pat = NULL; int patlen = 0, use_pattern = 0; dict *ht; /* 對象必須爲空(以迭代鍵名),或者對象的類型必須設置爲集合,排序集合或散列。*/ serverAssert(o == NULL || o->type == OBJ_SET || o->type == OBJ_HASH || o->type == OBJ_ZSET); /* 將i設置爲第一個選項參數。前一個是遊標。在對象爲空時第一個參數在第2個位置,不然爲第三個位置,例如:scan 0 ,sscan myset 0 match f*; */ i = (o == NULL) ? 2 : 3; /* Skip the key argument if needed. */
scan的實際操做一共分爲4步,下邊咱們來看下這四步。網絡
/* Step 1:解析選項. */ while (i < c->argc) { j = c->argc - i; // count選項,注意是從第二個開始 if (!strcasecmp(c->argv[i]->ptr, "count") && j >= 2) { if (getLongFromObjectOrReply(c, c->argv[i+1], &count, NULL) //獲取所傳遞的值count值並賦值給count,由於在count關鍵字後邊是count的值,因此爲c->argv[i+1]. != C_OK) { goto cleanup; //清理list等 } //若是count的值爲1,返回錯誤。清空在函數開頭建立的list。 if (count < 1) { addReply(c,shared.syntaxerr); goto cleanup; } i += 2; } else if (!strcasecmp(c->argv[i]->ptr, "match") && j >= 2) { // match選項,一樣是從第二個開始 pat = c->argv[i+1]->ptr; //獲取到匹配規則 patlen = sdslen(pat); /* 若是模式徹底是「*」,那麼它老是匹配的,因此這至關於禁用它。也就是說這種狀況下此模式無關緊要 */ use_pattern = !(pat[0] == '*' && patlen == 1); i += 2; } else { addReply(c,shared.syntaxerr); goto cleanup; } }
此步驟主要是對命令的解析,解析出count和match的值以及對相應變量的賦值,從而在下文過濾步驟中進行處理。數據結構
/* Step 2: 遍歷集合。 * *請注意,若是對象是用ziplist、intset或任何其餘非哈希表的表示進行編碼的,則能夠確定它也是由少許元素組成的。所以,爲了不獲取狀態,咱們只需在一次調用中返回對象內部的全部內容,將遊標設置爲0表示迭代結束。 */ /* 處理哈希表的狀況. 對應o的不一樣類型*/ ht = NULL; if (o == NULL) { ht = c->db->dict; } else if (o->type == OBJ_SET && o->encoding == OBJ_ENCODING_HT) { ht = o->ptr; } else if (o->type == OBJ_HASH && o->encoding == OBJ_ENCODING_HT) { ht = o->ptr; count *= 2; /* We return key / value for this type. */ } else if (o->type == OBJ_ZSET && o->encoding == OBJ_ENCODING_SKIPLIST) { zset *zs = o->ptr; ht = zs->dict; count *= 2; /* We return key / value for this type. */ } if (ht) { //通常的存儲,不是intset, ziplist void *privdata[2]; /*咱們將迭代的最大次數設置爲指定計數的10倍,所以若是哈希表處於病態狀態(很是稀疏地填充), 咱們將避免以返回沒有或不多元素爲代價來阻塞太多時間。 */ long maxiterations = count*10; /* 咱們向回調傳遞兩個指針:一個是它將向其中添加新元素的列表, 另外一個是包含dictionary的對象,以便可以以類型相關的方式獲取更多數據。 */ privdata[0] = keys; privdata[1] = o; do { //一個個掃描,從cursor開始,而後調用回調函數將數據設置到keys返回數據集裏面。 cursor = dictScan(ht, cursor, scanCallback, NULL, privdata); } while (cursor && maxiterations-- && listLength(keys) < (unsigned long)count); } else if (o->type == OBJ_SET) { //若是是set,將這個set裏面的數據所有返回,由於它是壓縮的intset,會很小的。 int pos = 0; int64_t ll; while(intsetGet(o->ptr,pos++,&ll)) listAddNodeTail(keys,createStringObjectFromLongLong(ll)); cursor = 0; } else if (o->type == OBJ_HASH || o->type == OBJ_ZSET) { //ziplist或者hash,字符串表示的數據結構,不會太大。 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 { serverPanic("Not handled encoding in SCAN."); }
此步驟根據不一樣的格式作出不一樣的處理,將掃描出來的元素放在list集合中,以方便過濾與取數。函數
/* Step 3: 過濾元素.此處是遍歷上文構造的list */ node = listFirst(keys); while (node) { robj *kobj = listNodeValue(node); nextnode = listNextNode(node); int filter = 0; /* 若是它不匹配的模式則過濾,此處的過濾是在上文給出. */ if (!filter && use_pattern) { if (sdsEncodedObject(kobj)) { if (!stringmatchlen(pat, patlen, kobj->ptr, sdslen(kobj->ptr), 0)) filter = 1; } else { char buf[LONG_STR_SIZE]; int len; serverAssert(kobj->encoding == OBJ_ENCODING_INT); len = ll2string(buf,sizeof(buf),(long)kobj->ptr); if (!stringmatchlen(pat, patlen, buf, len, 0)) filter = 1; } } /* 若是key過時,過濾. */ if (!filter && o == NULL && expireIfNeeded(c->db, kobj)) filter = 1; /* 若是須要過濾,刪除元素及其已設置的值. */ if (filter) { decrRefCount(kobj); listDelNode(keys, node); } /* 若是這是一個散列或排序集,咱們有一個鍵-值元素的平面列表,所以若是這個元素被過濾了, 那麼刪除這個值,或者若是它沒有被過濾,那麼跳過它:咱們只匹配鍵。*/ if (o && (o->type == OBJ_ZSET || o->type == OBJ_HASH)) { node = nextnode; nextnode = listNextNode(node); if (filter) { kobj = listNodeValue(node); decrRefCount(kobj); listDelNode(keys, node); } } node = nextnode; }
根據match參數過濾返回值,而且若是這個鍵已通過期也會直接過濾掉。最後返回元素。
/* Step 4: 返回消息給客戶端. */ addReplyMultiBulkLen(c, 2); addReplyBulkLongLong(c,cursor); addReplyMultiBulkLen(c, listLength(keys)); while ((node = listFirst(keys)) != NULL) { robj *kobj = listNodeValue(node); addReplyBulk(c, kobj); decrRefCount(kobj); listDelNode(keys, node); } //清理操做,清楚list等結構 cleanup: listSetFreeMethod(keys,decrRefCountVoid); listRelease(keys); }
綜上所述,scan能夠分爲四步:
redis-cli 提供一個bigkeys參數,能夠掃描redis中的大key
執行結果:
root@grape ~]# redis-cli --bigkeys # Scanning the entire keyspace to find biggest keys as well as # average sizes per key type. You can use -i 0.1 to sleep 0.1 sec # per 100 SCAN commands (not usually needed). [00.00%] Biggest string found so far 'testLuaSet' with 11 bytes [00.00%] Biggest string found so far 'number' with 18 bytes -------- summary ------- Sampled 2 keys in the keyspace! Total key length in bytes is 16 (avg len 8.00) Biggest string found 'number' has 18 bytes 2 strings with 29 bytes (100.00% of keys, avg size 14.50) 0 lists with 0 items (00.00% of keys, avg size 0.00) 0 sets with 0 members (00.00% of keys, avg size 0.00) 0 hashs with 0 fields (00.00% of keys, avg size 0.00) 0 zsets with 0 members (00.00% of keys, avg size 0.00)
此參數命令比較簡單,使用scan命令去遍歷全部的鍵,對每一個鍵根據其類型執行"STRLEN","LLEN","SCARD","HLEN","ZCARD"這些命令獲取其長度或者元素個數。
另外該方法有兩個缺點:
在redis中定義了一些opcode(1字節),去標記opcode以後保存的是什麼類型的數據,在這些類型中有一個value-type值類型,以下圖:
value_type就是值類型這一列,括號中的數字就是保存到rdb文件中時的實際使用數字。
咱們能夠寫代碼解析rdb文件,經過value_type去獲取每一個value的大小。
在這裏咱們推薦一個開源軟件:godis-cli-bigkey
詳情見github:https://github.com/erpeng/god...