Redis源码阅读之get命令
June 18, 2024
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);
}
可以看到,主要逻辑为:
- 查找对应的键,如果结果为NULL,则返回C_OK。
- 如果查到到的结果的类型不是OBJ_STRING,则返回C_ERR。
- 返回结果,返回错误码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;
}
其主要逻辑:
- 查找键dictFind。
- 判断是否需要做过期处理expireIfNeeded。
- 如果值不会NULL,且没有正在进行RDB快照生成操作,则更新访问时间。如果当前的内存套餐策略是LFU,则执行updateLFU(),否则,更新对象的lru字段。
- 返回值。
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;
}
逻辑:
- 调用keyIsExpired判断键是否过期。
- 如果是在副本中,则返回。
- 如果是在主节点中,则调用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;
}
主要逻辑:
字典长度是否为0,是则返回。
如果是在Rehash,则进行Rehash。
根据键获取hash key h。
根据h生成在数组中的下标值idx。
根据index查找到对应的dictEntry he。
判断he的键是否就是查找的键,如果是,则返回,否则,顺着链表继续查找。
这里第5步在数组中查找时,是两个数组ht_table[0],ht_table[1],这里主要是用于rehash,当写入的时候,如果当前正在rehash,写添加到ht_table[1]中,否则,添加到ht_table[0]中。