Redis 源码阅读:看看 GET 命令的实现

2,028 阅读3分钟

上一篇从 SET 命令开始,层层深入,了解了 Redis 的 SET 命令是怎么实现的。接下来看看 GET 命令的实现。

// File: s_string.c
// GET 命令是直接调用 getGenericCommand
void getCommand(client *c) {
    getGenericCommand(c);
}

int getGenericCommand(client *c) {
    robj *o;
    // 查找 Key
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.null[c->resp])) == NULL)
        return C_OK;
    // 判断 Key 对应的 Value 类型是不是 String
    if (checkType(c,o,OBJ_STRING)) {
        return C_ERR;
    }
    // 向客户端返回结果
    addReplyBulk(c,o);
    return C_OK;
}

getGenericCommand 函数比较短,在 getGenericCommand 中,我们能发现查询 key 的主要逻辑在 lookupKeyReadOrReply 函数。

// File: db.c
robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) {
    robj *o = lookupKeyRead(c->db, key);
    if (!o) addReplyOrErrorObject(c, reply);
    return o;
}

robj *lookupKeyRead(redisDb *db, robj *key) {
    return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);
}

robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
    // 断言:flags 中必须没有 LOOKUP_WRTIE 这个 flag
    serverAssert(!(flags & LOOKUP_WRITE));
    return lookupKey(db, key, flags);
}

robj *lookupKey(redisDb *db, robj *key, int flags) {
    // 在 dict 中寻找 key
    dictEntry *de = dictFind(db->dict,key->ptr);
    robj *val = NULL;
    // 如果 key 在 dict 中能找到
    if (de) {
	// 获取 key 对应的 value
        val = dictGetVal(de);
	// 这里进行和 flag 相关的一系列处理,暂时忽略
        /* 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 expire_flags = 0;
        if (flags & LOOKUP_WRITE && !is_ro_replica)
            expire_flags |= EXPIRE_FORCE_DELETE_EXPIRED;
        if (flags & LOOKUP_NOEXPIRE)
            expire_flags |= EXPIRE_AVOID_DELETE_EXPIRED;
	// 如果这个 key 到期了,那么令 val 为空,也就是逻辑上标记这个 val 是空的
        if (expireIfNeeded(db, key, expire_flags)) {
            /* The key is no longer valid. */
            val = NULL;
        }
    }

    // 如果 val 存在
    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. */
	// 更新 key 的访问时间
        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 */

    // 如果 val 不存在
    } else {
	// 对 keymiss 情况进行相应的记录和通知
        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;
}

根据 lookupKeyReadOrReply 函数的调用,我们发现查找 key 的主要逻辑在 lookupKey 函数中。lookupKey 函数通过 dictFind 来查找 dict 中存储的 key,通过 dictGetVal 来获取 key 对应的 value。

值得一提的是 expireIfNeeded 函数,lookupKey 函数中会通过 expireIfNeeded 来判断当前 key 是否过期。

// File: db.c
int expireIfNeeded(redisDb *db, robj *key, int flags) {
    // 如果 key 没有过期,则直接返回
    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.
     *
     * 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.
     *
     * When replicating commands from the master, keys are never considered
     * expired. */
    if (server.masterhost != NULL) {
        if (server.current_client == server.master) return 0;
        if (!(flags & EXPIRE_FORCE_DELETE_EXPIRED)) return 1;
    }

    /* In some cases we're explicitly instructed to return an indication of a
     * missing key without actually deleting it, even on masters. */
    if (flags & EXPIRE_AVOID_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. */
    if (checkClientPauseTimeoutAndReturnIfPaused()) return 1;

    // 删除 key
    deleteExpiredKeyAndPropagate(db,key);
    return 1;
}

expireIfNeeded 函数的主要作用是判断 key 是否过期,在满足某些条件后,过期的 key 将会被删除掉。

结合 lookupKey 函数的逻辑,我们能得到一个结论:Redis 对于 key 的 TTL 的处理是懒模式,也就是说当有 key 过期时,它不会被立刻删除。当有查询到这个 key 的时候才会被删除。但这样也有一个问题,如果某个过期 key 一直不被查询,那么它将一直占用内存空间。

推测:Redis 应该有其他的机制来解决这个问题。

总结

至此,我们了解了 GET 命令是如何实现查找某个 key 对应的 value。

其中 expireIfNeeded 函数还留下了一个疑问,Redis 是如何解决某个过期 key 一直占用内存空间的问题呢?相信随着阅读源码的深入,我们会在源码中找到答案。