当前位置: 首页 > 后端技术 > PHP

【Redis5源码学习】redis命令的key分析

时间:2023-03-29 21:32:17 PHP

baiyan命令语法命令含义:查找并返回所有匹配给定pattern的keypattern命令格式:KEYSpattern命令实战:127.0.0.1:6379>keys*1)"kkk"2)"key1"返回值:分析所有匹配到pattern的key的源码。keys命令对应的处理函数为keysCommand():voidkeysCommand(client*c){dictIterator*di;dictEntry*de;sdspattern=c->argv[1]->ptr;//获取我们的输入模式intplen=sdslen(pattern),allkeys;无符号长数字键=0;void*replylen=addDeferredMultiBulkLength(c);di=dictGetSafeIterator(c->db->dict);//初始化一个安全迭代器allkeys=(pattern[0]=='*'&&pattern[1]=='\0');//判断是否返回所有键的集合while((de=dictNext(di))!=NULL){//遍历整个键空间字典sdskey=dictGetKey(de);//获取键值robj*keyobj;//如果是返回所有key的集合,或者当前的key匹配我们给定的pattern,那么加入到返回列表中if(allkeys||stringmatchlen(pattern,plen,key,sdslen(key),0)){keyobj=createStringObject(key,sdslen(key));if(!keyIsExpired(c->db,keyobj)){//过滤掉没有过期的keyaddReplyBulk(c,keyobj);//添加到返回列表numkeys++;//返回键数++}decrRefCount(keyobj);}}dictReleaseIterator(di);//释放安全迭代器setDeferredMultiBulkLength(c,replylen,numkeys);//设置返回值的长度}由于我们使用了keys*命令,所以我们需要返回所有key的集合。我们先观察这段代码,它会使用一个安全的迭代器来遍历整个键空间字典。在遍历的时候,过滤掉那些匹配我们的模式并且没有过期的键,然后返回给客户端。由于其遍历的时间复杂度与字典的大小成正比,因此这会导致一个问题。当键很多时,键空间字典可能会很大。我们使用键一次性将字典从上到下移动。遍历一次会消耗很多时间。由于redis是单进程应用,长时间执行keys命令会阻塞redis进程,导致redis服务对外不可用。所以很多公司都会禁止开发者使用keys命令,这样可能会导致redis服务长时间不可用。参考案例:Redis的KEYS命令导致RDS数据库雪崩,RDS两次宕机,损失百万资金。那你可能会问,如果我把前面的*换成其他范围更小的模式,不就可以避免一次遍历所有的key空间了吗?但是我们看上面的源码,因为它在遍历每一个key的时候都会判断当前key是否匹配传入的pattern,所以并不是我们想象的那样,只是遍历了我们传入的patternkey空间元素的集合,但是它需要遍历完整的键空间集合,在遍历的同时过滤掉满足条件的键值。实际上遍历的初始范围并没有缩小,其时间复杂度仍然是O(N),其中N是键空间字典的大小。扩展安全迭代器和非安全迭代器在keys命令的遍历过程中涉及到安全迭代器的概念。相反,有不安全的迭代器。那么,迭代器是如何工作的,安全和不安全的区别是什么?我们先看一下迭代器的存储结构:typedefstructdictIterator{dict*d;//指向要遍历的字典longindex;//桶在哈希表中的索引位置inttable,safe;//表索引(指dict结构只能为0或1),以及迭代器是否安全来标记dictEntry*entry,*nextEntry;//存储当前条目和下一个条目longlong指纹;//指纹,只在非安全迭代Test的情况下做校对}dictIterator;为了让大家明白索引和表字段的作用,我们又要贴一下dict的结构:表字段只能是0或者1。0是正常情况下会用到的hash表。1是rehash过程中需要用到的transitionhashtable。而index就是01234567在每个哈希表中的索引。迭代器中的安全字段用于区分迭代器类型是否安全。所谓安全,就是在遍历过程中,对字典的操作不会影响遍历结果;非安全迭代器可能会因为rehash等操作导致遍历结果出错,但性能更好。怎么做到安全在redis中,safe迭代器通过直接禁止rehash操作来让迭代器安全。那么,为什么禁止rehash操作是安全的呢?我们都知道rehash操作是渐进的。每次执行命令时,都会进行重新哈希。rehash操作将使用字典的两个表。我们考虑这样一种情况:假设迭代器当前正在遍历第一个表,进度已经到了索引index为3的位置,并且某个元素还没有被rehash,而我们已经遍历了这个元素。然后同时进行rehash和遍历。假设rehash完成,这个元素已经到了第二个表中索引33的位置。目前迭代器的进度只走到了第二张表的index为13的位置,还没有遍历到index为33的位置。那么如果继续遍历的话,由于这个元素已经遍历了一次在第一个表中,现在不可避免地要进行第二次遍历。也就是说,由于rehash,同一个元素被遍历了两次,这就是rehash影响迭代器遍历结果的原因。为了解决上述问题,redis通过在安全迭代器运行过程中禁止rehash操作来保证迭代器安全。那么redis如何判断当前是否有安全迭代器在运行,进而禁止rehash操作呢?先回顾一下dict的结构:typedefstructdict{dictType*type;无效*私有数据;dicththt[2];//指向两个表的指针longrehashidx;//rehash标志,如果为-1,则不在rehashunsignedlong迭代器中;//当前运行的安全迭代器的数量}dict;我们看到字典结构中的iterators字段是用来描述安全迭代器的个数的。如果有一个安全的迭代器在运行,那么这个字段将为++。这样在迭代过程中,字典会变得相对稳定,避免了多次遍历一个元素的问题。如果当前运行的是安全迭代器,则迭代器字段不能为0。当该字段为0时,可以进行rehash操作:staticvoid_dictRehashStep(dict*d){if(d->iterators==0)dictRehash(d,1);}其实除了简单粗暴的safeiterator禁止rehash操作外,redis还提供了更高级的SCAN遍历方式。它使用更复杂、更巧妙的算法来保证即使在rehash过程中,遍历的结果也不会重复。这样保证了rehash操作和遍历操作可以并发执行,同时避免了key空间较大时遍历key时时间复杂度高导致redis阻塞的问题,大大提高了效率。安全的迭代器就一定安全吗?然后继续思考,不进行rehash操作就可以保证迭代器是安全的吗?由于redis是单进程应用,当我们执行keys命令时,会阻塞其他所有命令的执行。因此,迭代器在遍历时,我们无法通过外部执行命令来增删改keyspace字典。但是redis内部的一些时间事件可能会修改字典。例如:定时扫描某个key是否过期,如果过期则从key空间中删除。在这一点上,我认为即使是安全的迭代器也无法避免在遍历过程中可能对字典进行的操作。比如遍历的时候,redis在某个时间event删除了还没有遍历的元素,然后后面的迭代器继续遍历,这个元素就遍历不出来了。那么如何解决这个问题呢?除非redis在遍历过程中根本不触发事件和执行处理函数,否则这些操作都会导致遍历结果出现细微的错误,这是redis无法避免的。迭代器遍历的过程抛开上面的细节,我们接下来看一下具体的遍历逻辑。首先我们需要初始化安全迭代器:dictIterator*dictGetIterator(dict*d){dictIterator*iter=zmalloc(sizeof(*iter));iter->d=d;iter->表=0;iter->index=-1;iter->安全=0;iter->条目=空;iter->nextEntry=NULL;returniter;}如果是安全迭代器,除了初始化以上字段外,还需要将安全字段设置为1:dictIterator*dictGetSafeIterator(dict*d){dictIterator*i=dictGetIterator(d);//调用上面的方法初始化剩下的字段i->safe=1;//初始化安全字段returni;}回到开头的keys命令,调用dictGetSafeIterator()函数初始化一个安全迭代器。接下来,keys命令会循环调用dictNext()方法,遍历所有keyspace字典中的元素:dictEntry*dictNext(dictIterator*iter){while(1){//进入this的两种情况if://1.这是迭代器的第一次运行//2.当前索引链表中的节点已经被迭代过if(iter->entry==NULL){//指向遍历过的哈希表,默认是第一个哈希表dictht*ht=&iter->d->ht[iter->table];//只在第一次遍历时执行(索引初始化值为-1)if(iter->index==-1&&iter->table==0){//如果是安全迭代器(safe==1),然后更新迭代器计数器if(iter->safe)iter->d->iterators++;//如果是不安全迭代器,则计算指纹elseiter->fingerprint=dictFingerprint(iter->d);}//更新索引,继续遍历下一个桶上的元素iter->index++;//如果迭代器的当前索引大于当前迭代的哈希表大小//那么说明哈希表已经迭代了if(iter->index>=(signed)ht->size){//如果rehash操作正在进行中,这意味着第二个哈希表也在工作在使用中//然后继续遍历第二张哈希表if(dictIsRehashing(iter->d)&&iter->table==0){iter->table++;iter->index=0;ht=&iter->d->ht[1];//如果没有rehash,则不需要遍历第二张哈希表}else{break;}}//如果走到这里,说明哈希表还没有遍历//更新节点指针指向下一个索引链表的头节点(索引已经过了++)iter->entry=ht->表[iter->索引];}else{//执行到这里,说明遍历的是一个bucket上的链表(为了解决冲突,一个bucket后面会附加多个dictEntry,组成一个链表)iter->entry=iter->nextEntry;}//如果当前节点不为空,则记录该节点指向下一个节点(即next)的指针//因为安全迭代器在运行时可能会删除迭代器返回的当前节点,使得如果(iter->entry){iter->nextEntry=iter->entry->next;找不到下一个指针;返回iter->条目;}}//遍历完成returnNULL;}具体的遍历过程已经在注释的形式给出代码中有一个新的概念:fingerprint指纹。让我们讨论一下指纹的概念。指纹的作用是在dictNext()遍历函数中,有这么一段代码:if(iter->safe){//如果是安全迭代器(safe==1),则更新iteratorscounteriter->d->迭代器++;}else{//如果是不安全的迭代器,则计算指纹iter->fingerprint=dictFingerprint(iter->d);}我们看到当迭代器不安全时,它会验证一个指纹。顾名思义,insecure是指在遍历过程中可以进行rehash操作,这可能会导致遍历结果重复等问题。为了正确识别这类问题,redis采用了指纹机制,即在遍历前采集一个指纹,遍历完成后再采集一个指纹。如果两次指纹比对一致,说明遍历结果并没有因为rehash操作的影响而改变。那么如何验证指纹呢?指纹验证的本质是判断字典是否因为rehash操作而改变:longlongdictFingerprint(dict*d){longlongintegers[6],hash=0;诠释j;整数[0]=(long)d->ht[0].table;整数[1]=d->ht[0].size;整数[2]=d->ht[0].used;整数[3]=(long)d->ht[1].table;整数[4]=d->ht[1].size;整数[5]=d->ht[1].used;for(j=0;j<6;j++){hash+=整数[j];/*对于散列步骤,我们使用TomasWang的64位整数散列。*/hash=(~hash)+(hash<<21);//hash=(hash<<21)-hash-1;hash=hash^(hash>>24);散列=(散列+(散列<<3))+(散列<<8);//hash*265hash=hash^(hash>>14);散列=(散列+(散列<<2))+(散列<<4);//hash*21hash=hash^(hash>>28);散列=散列+(散列<<31);}返回散列;}我们看到指纹校验是根据字典的table,size,used等字段。如果这些字段发生变化,则表示正在执行或完成重新哈希操作。一旦进行rehash操作,可能会影响遍历结果。因此,非安全迭代器的指纹验证可以很好地发现rehash操作影响遍历结果的可能性。