baiyanredis
命令含义:从当前选定数据库随机返回一个key
命令格式:算法
RANDOMKEY
命令实战:数据库
127.0.0.1:6379> keys * 1) "kkk" 2) "key1" 127.0.0.1:6379> randomkey "key1" 127.0.0.1:6379> randomkey "kkk"
返回值: 随机的键;若是数据库为空则返回nildom
keys命令对应的处理函数是randomKeyCommand():函数
void randomkeyCommand(client *c) { robj *key; // 存储获取到的key if ((key = dbRandomKey(c->db)) == NULL) { // 调用核心函数dbRandomKey() addReply(c,shared.nullbulk); // 返回nil return; } addReplyBulk(c,key); // 返回key decrRefCount(key); // 减小引用计数 }
randomKeyCommand()调用了dbRandomKey()函数来真正生成一个随机键:源码分析
robj *dbRandomKey(redisDb *db) { dictEntry *de; int maxtries = 100; int allvolatile = dictSize(db->dict) == dictSize(db->expires); while(1) { sds key; robj *keyobj; de = dictGetRandomKey(db->dict); // 获取随机的一个dictEntry if (de == NULL) return NULL; // 获取失败返回NULL key = dictGetKey(de); // 获取dictEntry中的key keyobj = createStringObject(key,sdslen(key)); // 根据key字符串生成robj if (dictFind(db->expires,key)) { // 去过时字典里查找这个键 ... if (expireIfNeeded(db,keyobj)) { // 判断键是否过时 decrRefCount(keyobj); // 若是过时了,删掉这个键并减小引用计数 continue; // 当前键过时了不能返回,只返回不过时的键,进行下一次随机生成 } } return keyobj; } }
那么这一层的主逻辑又调用了dictGetRandomKey(),获取随机的一个dictEntry。假设咱们已经获取到了随机生成的dictEntry,咱们随后取出key。因为不能返回过时的key,因此咱们须要先判断键是否过时,若是过时了就不能返回了,直接continue;若是不过时就能够返回。spa
那么咱们继续跟进dictGetRandomKey()函数,看一下究竟使用了什么算法,来随机生成dictEntry:3d
dictEntry *dictGetRandomKey(dict *d) { dictEntry *he, *orighe; unsigned long h; int listlen, listele; if (dictSize(d) == 0) return NULL; // 传进来的字典为空,根本不用生成 if (dictIsRehashing(d)) _dictRehashStep(d); // 执行一次rehash操做 if (dictIsRehashing(d)) { // 若是正在rehash,注意要保证从两个哈希表中均匀分配随机种子 do { h = d->rehashidx + (random() % (d->ht[0].size +d->ht[1].size - d->rehashidx)); //计算随机哈希值,这个哈希值必定是在rehashidx的后部 he = (h >= d->ht[0].size) ? d->ht[1].table[h - d->ht[0].size] : d->ht[0].table[h];// 根据上面计算的哈希值拿到对应的bucket } while(he == NULL); // 一直循环计算,取最后一个计算结果不为空的bucket } else { // 不在rehash,只有一个哈希表 do { h = random() & d->ht[0].sizemask; // 直接计算哈希值 he = d->ht[0].table[h]; // 取出哈希表上第h个bucket } while(he == NULL); // 一直循环计算,取最后一个计算结果不为空的bucket } // 如今咱们获得了一个不为空的bucket,而这个bucket的后面还挂接了一个或多个dictEntry(链地址法解决哈希冲突),因此一样须要计算一个随机索引,来判断究竟访问哪个dickEntry链表结点 listlen = 0; orighe = he; while(he) { he = he->next; listlen++; // 计算链表长度 } listele = random() % listlen; // 随机数对链表长度取余,肯定获取哪个结点 he = orighe; while(listele--) he = he->next; // 从前到后遍历这个bucket上的链表,找到这个结点 return he; // 最终返回这个结点 }
这个函数首先会进行字典为空的判断。而后会进行一个单步rehash操做,这一点和调用如dictAdd()等字典函数的效果是同样的,都是渐进式rehash技术的一部分。在这里咱们首先复习一下字典的总体结构:
因为rehash会影响随机数种子的生成,因此根据当前字典是否正在进行rehash操做,须要分两种状况讨论:
第一种:正在进行rehash操做。 那么当前字典的结构为:有一部分键在第一个哈希表上、其他的键在第二个哈希表上。为了均匀分配两个哈希表可能被取到的几率,须要将两个哈希表结合考虑。其算法为:code
h = d->rehashidx + (random() % (d->ht[0].size + d->ht[1].size - d->rehashidx)); //计算随机哈希值,这个哈希值必定是在rehashidx的后部
这里将一个随机数对两个哈希表大小之和减去rehashidx取余。这样的取余操做能够保证这个哈希值会随机落在索引大于rehashidx位置的bucket上。由于rehashidx表示rehash的进度。这个rehashidx表示在第一个哈希表上在这个索引以前的数据,即[0, rehashidx-1],这个闭区间上的数据已经在被rehash到第二个哈希表上了。而大于等于这个rehashidx的元素仍在第一个哈希表上。因此,这样就保证了任何一个结果h上的bucket,都是非空有值的。接下来只须要判断这个h值在哪一个哈希表上,而后去哈希表上对应位置上的bucket取值便可:blog
he = (h >= d->ht[0].size) ? d->ht[1].table[h - d->ht[0].size] : d->ht[0].table[h];
第二种:没有进行rehash操做。 那么全部键都在惟一一个第一个字典上,这种状况就很是简单了,能够直接对字典长度求余,或者对字典的sizemask进行按位与运算,均可以保证计算后的结果落在哈希表内。redis选择的是后者:
h = random() & d->ht[0].sizemask; // 经过对sizemask的按位与运算计算哈希值 he = d->ht[0].table[h]; // 取出哈希表上第h个bucket
接下来,咱们找到了一个非空的bucket,可是尚未结束。因为可能存在哈希冲突,redis采用链地址法解决哈希冲突,因此会在一个bucket后面挂接多个dictEntry,造成一个链表。因此,还须要思考究竟要取哪个链表结点上的dictEntry。这个算法就比较简单了,直接利用random()的结果,对链表长度求余便可:
listele = random() % listlen; // 随机数对链表长度取余,肯定获取哪个结点 while(listele--) he = he->next; // 从前到后遍历这个bucket上的链表,找到这个结点
到此为止,咱们就找到了一个随机bucket上的一个随机dictEntry结点,那么就能够返回给客户端啦。