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...