Redis源码阅读之get命令

Redis源码阅读之get命令

June 18, 2024
Redis, 数据库
Redis
redis版本:7.0.2

get命令对应源码中的函数名getCommand,对应文件是t_string.c。相关源码如下:

int getGenericCommand(client *c) {

robj *o;

  

if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.null[c->resp])) == NULL)

return C_OK;

  

if (checkType(c,o,OBJ_STRING)) {

return C_ERR;

}

  

addReplyBulk(c,o);

return C_OK;

}

  

void getCommand(client *c) {

getGenericCommand(c);

}

可以看到,主要逻辑为:

  1. 查找对应的键,如果结果为NULL,则返回C_OK。
  2. 如果查到到的结果的类型不是OBJ_STRING,则返回C_ERR。
  3. 返回结果,返回错误码C_OK。

其中查找函数为lookupKeyReadOrReply。

lookupKeyReadOrReply #

这个函数最终会调用函数lookupkey,位于文件db.c中:

/* Lookup a key for read or write operations, or return NULL if the key is not
 * found in the specified DB. This function implements the functionality of
 * lookupKeyRead(), lookupKeyWrite() and their ...WithFlags() variants.
 * 在读操作或写操作中查找key,如果在指定DB中没有找到相关key,则返回NULL。这个函数实现了lookupKeyRead(),
 * lookupKeyWrite()和它们的...WithFlags()变体的功能(这两个函数会调用本函数)。
 *
 * Side-effects of calling this function:
 *
 * 1. A key gets expired if it reached it's TTL.
 * 2. The key's last access time is updated.
 * 3. The global keys hits/misses stats are updated (reported in INFO).
 * 4. If keyspace notifications are enabled, a "keymiss" notification is fired.
 * 本函数的作用:
 * 1. 如果key的TTL到期,则key会过期。
 * 2. 更新key的最后访问时间。
 * 3. 更新全局的key hits/misses统计信息(在INFO中报告)。
 * 4. 如果启用了keyspace通知,会触发一个“keymiss”通知。
 *
 * Flags change the behavior of this command:
 *
 *  LOOKUP_NONE (or zero): No special flags are passed.
 *  LOOKUP_NOTOUCH: Don't alter the last access time of the key.
 *  LOOKUP_NONOTIFY: Don't trigger keyspace event on key miss.
 *  LOOKUP_NOSTATS: Don't increment key hits/misses counters.
 *  LOOKUP_WRITE: Prepare the key for writing (delete expired keys even on
 *                replicas, use separate keyspace stats and events (TODO)).
 * 影响本函数功能的相关标志:
 * LOOKUP_NONE(或0):没有传递特殊标志。
 * LOOKUP_NOTOUCH:不更改key的最后访问时间。
 * LOOKUP_NONOTIFY:在key miss时不触发keyspace事件。
 * LOOKUP_NOSTATS:不更新key hits/misses计数器。
 * LOOKUP_WRITE:准备key进行写操作(即使在副本上也删除过期key,使用单独的keyspace统计信息和事件(TODO))。注意:在* 只读副本上不会删除。
 *
 * Note: this function also returns NULL if the key is logically expired but
 * still existing, in case this is a replica and the LOOKUP_WRITE is not set.
 * Even if the key expiry is master-driven, we can correctly report a key is
 * expired on replicas even if the master is lagging expiring our key via DELs
 * in the replication link. 
 * 注意:如果key在逻辑上已过期但实际上仍然存在,本函数会返回NULL。
 * */
robj *lookupKey(redisDb *db, robj *key, int flags)
{
    dictEntry *de = dictFind(db->dict, key->ptr);
    robj *val = NULL;
    if (de)
    {
        val = dictGetVal(de);
        /* Forcing deletion of expired keys on a replica makes the replica
         * inconsistent with the master. We forbid it on readonly replicas, but
         * we have to allow it on writable replicas to make write commands
         * behave consistently.
         * 在一个副本上强制删除过期键会导致副本与主节点不一致。我们禁止只读副本上的这种操作,但是在可写副本上
         * 我们必须允许这种操作,这样才能使写命令的结果保持一致。
         *
         * It's possible that the WRITE flag is set even during a readonly
         * command, since the command may trigger events that cause modules to
         * perform additional writes. */
        int is_ro_replica = server.masterhost && server.repl_slave_ro;
        int force_delete_expired = flags & LOOKUP_WRITE && !is_ro_replica;
        if (expireIfNeeded(db, key, force_delete_expired))
        {
            /* The key is no longer valid. */
            val = NULL;
        }
    }

    if (val)
    {
        /* Update the access time for the ageing algorithm.
         * Don't do it if we have a saving child, as this will trigger
         * a copy on write madness. */
        if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH))
        {
            if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU)
            {
                updateLFU(val);
            }
            else
            {
                val->lru = LRU_CLOCK();
            }
        }

        if (!(flags & (LOOKUP_NOSTATS | LOOKUP_WRITE)))
            server.stat_keyspace_hits++;
        /* TODO: Use separate hits stats for WRITE */
    }
    else
    {
        if (!(flags & (LOOKUP_NONOTIFY | LOOKUP_WRITE)))
            notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
        if (!(flags & (LOOKUP_NOSTATS | LOOKUP_WRITE)))
            server.stat_keyspace_misses++;
        /* TODO: Use separate misses stats and notify event for WRITE */
    }

    return val;
}

其主要逻辑:

  1. 查找键dictFind。
  2. 判断是否需要做过期处理expireIfNeeded。
  3. 如果值不会NULL,且没有正在进行RDB快照生成操作,则更新访问时间。如果当前的内存套餐策略是LFU,则执行updateLFU(),否则,更新对象的lru字段。
  4. 返回值。

expireIfNeeded 位于db.c文件中,源码如下:

/* This function is called when we are going to perform some operation
 * 当我们要对给定键执行某些操作时会调用此函数。但是即使给定键仍然存在于数据库中,它可能在逻辑上已经过期了。
 * in a given key, but such key may be already logically expired even if
 * it still exists in the database. The main way this function is called
 * is via lookupKey*() family of functions.
 * 此函数主要是通过lookupKey*()系列函数调用。
 *
 * The behavior of the function depends on the replication role of the
 * instance, because by default replicas do not delete expired keys. They
 * wait for DELs from the master for consistency matters. However even
 * replicas will try to have a coherent return value for the function,
 * so that read commands executed in the replica side will be able to
 * behave like if the key is expired even if still present (because the
 * master has yet to propagate the DEL).
 * 这个函数的行为取决于实例在主从集群中的角色,因为默认情况下副本不会删除过期键。为了一致性,
 * 副本会等待从主节点传递过来的DEL命令。然而即使是副本也会尝试返回一致的返回值,这样即使
 * 键仍然存在(因为还没有收到master传递过来的DEL命令),在副本中执行read命令时,效果也会和
 * 键已过期了一样。
 *
 * In masters as a side effect of finding a key which is expired, such
 * key will be evicted from the database. Also this may trigger the
 * propagation of a DEL/UNLINK command in AOF / replication stream.
 * 在主节点上找到一个过期键时,这个键会被从database中删除(evicted)。这也会导致在AOF/复制流中
 * 传播DEL/UNLINK命令。
 *
 * On replicas, this function does not delete expired keys by default, but
 * it still returns 1 if the key is logically expired. To force deletion
 * of logically expired keys even on replicas, set force_delete_expired to
 * a non-zero value. Note though that if the current client is executing
 * replicated commands from the master, keys are never considered expired.
 * 在副本中,默认情况下,这个函数不会删除过期键,但是如果这个键过期了,它仍然会返回1.要在副本上
 * 强制删除过期键,需要将force_delete_expired设置为非零值。但是请注意,如果当前客户端正在执行
 * 主节点传递过来的复制命令,那么键永远不会被视为过期的。 
 *
 *
 * The return value of the function is 0 if the key is still valid,
 * otherwise the function returns 1 if the key is expired. 
 * 如果key仍然有效,会返回0,否则返回1。
 * */
int expireIfNeeded(redisDb *db, robj *key, int force_delete_expired)
{
    if (!keyIsExpired(db, key))
        return 0;

    /* If we are running in the context of a replica, instead of
     * evicting the expired key from the database, we return ASAP:
     * the replica key expiration is controlled by the master that will
     * send us synthesized DEL operations for expired keys. The
     * exception is when write operations are performed on writable
     * replicas.
     * 如果我们是在副本中运行,那我们会尽快返回,而不是将过期键从database中删除:
     * 副本中键的过期机制受主节点控制,主节点会向我们发送过期键的DEL操作(合成)。但
     * 是在可写副本上执行写操作时除外。
     *
     * Still we try to return the right information to the caller,
     * that is, 0 if we think the key should be still valid, 1 if
     * we think the key is expired at this time.
     * 我们仍然会尝试返回正确的信息给调用者,即如果我们认为键仍然有效,则返回0,否则返回1.
     *
     * When replicating commands from the master, keys are never considered
     * expired.
     * 当复制/执行来自于主节点的命令时,键永远不会被视为过期的。
     *  */
    if (server.masterhost != NULL)
    {
        if (server.current_client == server.master)
            return 0;
        if (!force_delete_expired)
            return 1;
    }

    /* If clients are paused, we keep the current dataset constant,
     * but return to the client what we believe is the right state. Typically,
     * at the end of the pause we will properly expire the key OR we will
     * have failed over and the new primary will send us the expire.
     * 如果客户端被paused(挂起),我们会保持当前数据集不变,但是会返回给客户端我们认为的正确状态。
     * 通常,当pause结束时,我们会删除过期键或者失败,然后新的主节点会发送过期键(删除命令)。
     *  */
    if (checkClientPauseTimeoutAndReturnIfPaused())
        return 1;

    /* Delete the key
     * 删除键
     */
    deleteExpiredKeyAndPropagate(db, key);
    return 1;
}

逻辑:

  1. 调用keyIsExpired判断键是否过期。
  2. 如果是在副本中,则返回。
  3. 如果是在主节点中,则调用deleteExpiredKeyAndPropagate删除过期键。我们在后面再详细解析deleteExpiredKeyAndPropagate这个函数对应的源码。

updateLFU 位于db.c文件中,源码如下:

/* Update LFU when an object is accessed.

* Firstly, decrement the counter if the decrement time is reached.

* Then logarithmically increment the counter, and update the access time. */

void updateLFU(robj *val)

{

unsigned long counter = LFUDecrAndReturn(val);

counter = LFULogIncr(counter);

val->lru = (LFUGetTimeInMinutes() << 8) | counter;

}

这里的LFU其实是和redis的内存淘汰策略相关。

dictFind #

在全局字典中查找键,位于dict.c。

dictEntry *dictFind(dict *d, const void *key)

{

dictEntry *he;

uint64_t h, idx, table;

  

if (dictSize(d) == 0)

return NULL; /* dict is empty */

if (dictIsRehashing(d))

_dictRehashStep(d);

h = dictHashKey(d, key);

for (table = 0; table <= 1; table++)

{

idx = h & DICTHT_SIZE_MASK(d->ht_size_exp[table]);

he = d->ht_table[table][idx];

while (he)

{

if (key == he->key || dictCompareKeys(d, key, he->key))

return he;

he = he->next;

}

if (!dictIsRehashing(d))

return NULL;

}

return NULL;

}

主要逻辑:

  1. 字典长度是否为0,是则返回。

  2. 如果是在Rehash,则进行Rehash。

  3. 根据键获取hash key h。

  4. 根据h生成在数组中的下标值idx。

  5. 根据index查找到对应的dictEntry he。

  6. 判断he的键是否就是查找的键,如果是,则返回,否则,顺着链表继续查找。

    这里第5步在数组中查找时,是两个数组ht_table[0],ht_table[1],这里主要是用于rehash,当写入的时候,如果当前正在rehash,写添加到ht_table[1]中,否则,添加到ht_table[0]中。