葡萄命令语法命令含义:增量迭代一个集合元素。命令格式:SCANcursor[MATCHpattern][COUNTcount]命令实战:基本执行traverseredis127.0.0.1:6379>scan01)"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”redis127.0.0.1:6379>扫描171)“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选项的作用是让用户告诉迭代命令每次迭代应从数据集中返回多少元素。使用COUNT选项相当于增量迭代命令的提示。在大多数情况下,此提示在控制返回值数量方面更为有效。COUNT参数的默认值为10。当数据集比较大时,如果不使用MATCH选项,命令返回的元素个数通常与COUNT选项指定的数目相同,或者略多于COUNT选项指定的数目。在编码为整数(intset,仅包含整数值的小集合)或编码为压缩列表(ziplist,小散列或小的不同值的小集合)时迭代迭代命令通常忽略指定的值通过COUNT选项,并在第一次迭代时将数据集中包含的所有元素返回给用户。注意:不必为每次迭代使用相同的COUNT值。用户可以在每次迭代中根据自己的需要自由更改COUNT值,只要记得在下一次迭代中使用上一次迭代返回的游标即可。127.0.0.1:6379>scan0count21)"12"2)1)"user_level_1"2)"mykey"127.0.0.1:6379>MATCH选项类似于KEYS命令,增量迭代命令通过给定MATCH参数该方法通过提供一个glob样式的模式参数来实现,因此该命令仅返回与给定模式匹配的元素。redis127.0.0.1:6379>saddmyset123foofoobarfeelsgood(integer)6redis127.0.0.1:6379>sscanmyset0matchf*1)"0"2)1)"foo"2)"feelsgood"3)"foobar"redis127.0.0.1:6379>返回值:SCAN、SSCAN、HSCAN和ZSCAN命令都返回一个包含两个元素的多批量回复:回复的第一个元素是一个无符号的64位整数,由字符串表示(游标),回复的第二个元素是另一个多批量回复,包含本次迭代的元素。源码分析本文以扫描命令为例。命令入口/*SCAN命令完全依赖于scanGenericCommand。*/voidscanCommand(client*c){unsignedlongcursor;如果(parseScanCursorOrReply(c,c->argv[1],&cursor)==C_ERR)返回;scanGenericCommand(c,NULL,cursor);}Processcursors/*尝试解析存储在对象“o”中的扫描光标:如果光标有效,*将其作为无符号整数存储到*cursor中并返回C_OK。否则返回C_ERR并向客户端发送错误。*这里o->ptr存储我们的输入光标*/intparseScanCursorOrReply(client*c,robj*o,unsignedlong*cursor){char*eptr;/*使用strtoul(),因为我们需要一个unsignedlong,*所以getLongLongFromObject()不会覆盖整个游标空间。*/错误号=0;*cursor=strtoul(o->ptr,&eptr,10);if(isspace(((char*)o->ptr)[0])||eptr[0]!='\0'||errno==ERANGE){addReplyError(c,"无效游标");返回C_ERR;}returnC_OK;}scanpublicfunctionvoidscanGenericCommand(client*c,robj*o,unsignedlongcursor){inti,j;列表*keys=listCreate();listNode*节点,*下一个节点;长计数=10;sdspat=NULL;intpatlen=0,use_pattern=0;听写*ht;name),或者对象的类型必须设置为set、sortedset或hash。*/serverAssert(o==NULL||o->type==OBJ_SET||o->type==OBJ_HASH||o->type==OBJ_ZSET);/*将i设置为第一个选项参数。前一个是游标。当对象为空时,第一个参数在第二个位置,否则在第三个位置,例如:scan0,sscanmyset0matchf*;*/我=(o==NULL)?2:3;/*如果需要,跳过关键参数。*/scan的实际操作分为4个步骤。让我们看看下面这4个步骤。step1:解析命令选项/*第一步:解析选项。*/while(iargc){j=c->argc-i;//计数选项,注意从第二个开始i+1],&count,NULL)//获取传过来的值count值赋值给count,因为count的值在count关键字后面,所以是c->argv[i+1].!=C_OK){转到清理;//清理list等}//如果count的值为1,则返回错误。清空在函数开头创建的列表。if(count<1){addReply(c,shared.syntaxerr);转到清理;}我+=2;}elseif(!strcasecmp(c->argv[i]->ptr,"match")&&j>=2){//匹配选项,也是从第二个开始pat=c->argv[i+1]->指针;//获取匹配规则patlen=sdslen(pat);/*如果模式完全是“*”,那么它总是匹配的,所以这相当于禁用它。也就是说,这种模式在这种情况下是可选的*/use_pattern=!(pat[0]=='*'&&patlen==1);我+=2;}else{addReply(c,shared.syntaxerr);转到清理;这一步主要是解析command,解析出count和match的值,并给相应的变量赋值,在后面的过滤步骤中进行处理。step2:遍历集合构造列表/*Step2:遍历集合。**请注意,如果一个对象被编码为ziplist、intset或任何其他非哈希表表示形式,则可以肯定它也由少量元素组成。因此,为了避免获取状态,我们只需在一次调用中返回对象内部的所有内容,将光标设置为0以指示迭代结束。*//*处理哈希表的情况。对应不同类型的o*/ht=NULL;如果(o==NULL){ht=c->db->dict;}elseif(o->type==OBJ_SET&&o->encoding==OBJ_ENCODING_HT){ht=o->ptr;}elseif(o->type==OBJ_HASH&&o->encoding==OBJ_ENCODING_HT){ht=o->ptr;数*=2;/*我们返回此类型的键/值。*/}elseif(o->type==OBJ_ZSET&&o->encoding==OBJ_ENCODING_SKIPLIST){zset*zs=o->ptr;ht=zs->dict;数*=2;/*我们返回此类型的键/值。*/}if(ht){//一般存储,不是intset,ziplistvoid*privdata[2];/*我们将迭代的最大次数设置为指定计数的10倍,因此如果哈希表出现病态(非常稀疏),我们可以避免以不返回元素或返回极少元素为代价阻塞太多时间。*/longmaxiterations=count*10;/*我们向回调传递两个指针:一个是它将添加新元素的列表,另一个是包含字典的对象,以便能够以类型相关的方式获取更多数据。*/privdata[0]=键;私有数据[1]=o;do{//一条一条扫描,从游标开始,然后调用回调函数将数据置入返回的keys数据集中。cursor=dictScan(ht,cursor,scanCallback,NULL,privdata);}while(cursor&&maxiterations--&&listLength(keys)<(unsignedlong)count);}elseif(o->type==OBJ_SET){//如果是set,则返回这个set中的所有数据,因为是压缩的intset,会很小。int位置=0;int64_tll;while(intsetGet(o->ptr,pos++,&ll))listAddNodeTail(keys,createStringObjectFromLongLong(ll));游标=0;}elseif(o->type==OBJ_HASH||o->type==OBJ_ZSET){//ziplist或hash,字符串表示的数据结构,不会太大。unsignedchar*p=ziplistIndex(o->ptr,0);无符号字符*vstr;无符号整数vlen;长长的;while(p){//扫描整个key,然后集中返回一个。并将光标归为0,表示没有剩余。其实这相当于没有遍历ziplistGet(p,&vstr,&vlen,&vll);listAddNodeTail(keys,(vstr!=NULL)?createStringObject((char*)vstr,vlen):createStringObjectFromLongLong(vll));p=ziplistNext(o->ptr,p);}游标=0;}else{serverPanic("未在SCAN中处理编码。");}这一步根据不同的格式做不同的处理,将扫描到的元素放入列表集合中,方便过滤和抓取。step3:Filterelements/*第三步:过滤元素。这里就是遍历上面构造的list*/node=listFirst(keys);while(node){robj*kobj=listNodeValue(node);nextnode=listNextNode(节点);内部过滤器=0;/*如果不符合pattern就过滤,这里的过滤在上面给出。*/if(!filter&&use_pattern){if(sdsEncodedObject(kobj)){if(!stringmatchlen(pat,patlen,kobj->ptr,sdslen(kobj->ptr),0))filter=1;}else{charbuf[LONG_STR_SIZE];国际长度;服务器断言(kobj->编码==OBJ_ENCODING_INT);len=ll2string(buf,sizeof(buf),(long)kobj->ptr);如果(!stringmatchlen(pat,patlen,buf,len,0))filter=1;}}/*如果密钥过期,过滤。*/if(!filter&&o==NULL&&expireIfNeeded(c->db,kobj))filter=1;/*如果需要过滤,则删除该元素及其设置的值。*/if(filter){decrRefCount(kobj);listDelNode(键,节点);}/*如果这是一个散列集或排序集,我们有一个简单的键值元素列表,所以如果这个元素被过滤,则删除该值,或者如果它没有被过滤,则跳过它:我们只匹配键*/if(o&&(o->type==OBJ_ZSET||o->type==OBJ_HASH)){node=nextnode;nextnode=listNextNode(节点);如果(过滤器){kobj=listNodeValue(节点);decrRefCount(kobj);listDelNode(键,节点);}}节点=下一个节点;}根据match参数过滤返回值,如果key已经过期,直接过滤掉。返回末尾的元素。step4:Returnmessagetoclient+cleanup/*Step4:返回消息给client。*/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(键,节点);}//清理操作,清除list等结构cleanup:listSetFreeMethod(keys,decrRefCountVoid);listRelease(keys);}总结起来,scan可以分为四个步骤:解析count和匹配参数。如果不指定count,默认返回10条数据开始迭代集合。如果是keytosave如果是ziplist或intset,会一次性返回所有数据,不带游标(游标值直接返回0)。由于redis设计只有在数据量比较少的时候才保存为ziplist或者intset,所以这里不会影响性能。游标在另存为哈希时起作用。具体的入口函数是dictScan。详见dictScan根据match参数过滤返回值的原理,如果key已经过期则直接过滤掉(redis中的key过期后不会立即删除,这里有两个过期删除涉及redis机制,惰性删除和周期性删除)返回结果给客户端,是一个数组,第一个值是一个游标,第二个值是一个特定的键值对扩展查找大键的方法:bigkeys参数redis-cli提供了一个bigkeys参数,可以扫描redis中bigkey的执行结果:root@grape~]#redis-cli--bigkeys#扫描整个keyspace寻找最大的key以及#averagesizesper密钥类型。您可以使用-i每100个SCAN命令0.1秒睡眠0.1秒(通常不需要)。[00.00%]迄今为止发现的最大字符串“testLuaSet”有11个字节[00.00%]迄今为止发现的最大字符串“数字”有18个字节--------总结--------在键空间中采样了2个键!以字节为单位的总键长度为16(平均len8.00)找到的最大字符串“数字”有18个字节2个字符串有29个字节(100.00%的键,平均大小14.50)0个包含0个项目的列表(00.00%的键,平均大小0.00)0个包含0个成员的集合(00.00%的键,平均大小0.00)0个包含0个字段的哈希(00.00%的键,平均大小0.00)0zsetswith0members(00.00%ofkeys,avgsize0.00)这个参数命令比较简单,使用scan命令遍历所有的key,根据每个key执行"STRLEN","LLEN","SCARD",对其类型HLEN"、"ZCARD"命令获取其长度或元素数。另外,这种方式有两个缺点:1.在线使用:scan命令虽然通过游标遍历空间,可以在生产中的slave服务上执行,但毕竟是在线操作2.set,zset,list和hash类型只能获取元素的个数。但实际上,很多元素并不一定会占用很多空间。在redis中通过RDB文件定义了一些opcodes(1字节),用来标记在opcode之后保存的是什么类型的数据。在这些类型中,有一个value-type值类型。如下图所示:value_type为值类型列,括号中的数字为保存到rdb文件时实际使用的数字。我们可以写代码解析rdb文件,通过value_type获取每个value的大小。这里给大家推荐一款开源软件:godis-cli-bigkey详细可以看github:https://github.com/erpeng/god...scan的优缺点提供key空间遍历操作,支持游标,有O(1)的复杂度,整体遍历只需要O(N);提供结果模式匹配;支持设置一次返回的数据条数,但只是提示,有时会返回更多;弱状态,所有状态只需要由客户端Cursor维护;无法提供完整的快照遍历,即如果中间有数据修改,部分参与修改的数据可能无法遍历;每次返回的数据数量不一定,极度依赖内部实现;返回的数据可能重复,应用层必须能够处理可重入逻辑;count是每次扫描的键数,而不是结果集的数量。计数取决于扫描数据的大小。Scan虽然是无锁的,但是并不能保证百万级以上数据级别的查找效率。计数不能太小,网络交互会增加。计数应尽可能大。如果搜索结果集小于10000条,建议直接设置为与采集的一样大。参考文章RedisScan的使用方法和Springredis的坑Redisscan命令的原理