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结点,那么就可以返回给客户端啦。


NoSay
449 声望544 粉丝