上一篇从 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 一直占用内存空间的问题呢?相信随着阅读源码的深入,我们会在源码中找到答案。