Redis5源码学习浅析redis命令之randomkey篇

36次阅读

共计 3341 个字符,预计需要花费 9 分钟才能阅读完成。

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

正文完
 0