baiyan
命令语法
命令含义:从当前选定数据库随机返回一个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"
返回值: 随机的键;如果数据库为空则返回nil
源码分析
主体流程
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;如果不过期就可以返回。
真正获取随机键的算法
那么我们继续跟进dictGetRandomKey()函数,看一下究竟使用了什么算法,来随机生成dictEntry:
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操作。 那么当前字典的结构为:有一部分键在第一个哈希表上、其余的键在第二个哈希表上。为了均匀分配两个哈希表可能被取到的概率,需要将两个哈希表结合考虑。其算法为:
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取值即可:
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结点,那么就可以返回给客户端啦。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。