Redis 源码阅读:从 SET 命令开始

1,116 阅读4分钟

Redis 是什么?

Redis 是一个开源的内存数据结构存储器,可以被用做数据库、缓存、消息队列和流处理引擎。Redis 提供了字符串、哈希表、列表、集合、支持范围查询的有序集合、位图、地理信息索引等数据结构。内建支持主从复制、Lua 脚本、LRU 淘汰机制、事务和多种级别的硬盘持久化能力。 —— 摘抄并翻译自 Redis 官网

简单的说,Redis 是一款内存数据库,其管理的所有数据都存储在内存中。Redis 实现了很多内存数据结构以便用户能够在 Redis 上方便的构建各种业务逻辑。

在 Redis 中,最基础的数据结构莫过于字符串(strings)了,在 redis-cli 中,可以这样操作 strings 结构:

> SET mykey somevalue 
OK 
> GET mykey 
"somevalue" 

上述命令中,我们使用 SET 命令将 mykey → somevalue 这个键值对存入了 Redis,并使用 GET 命令查询到了 mykey 所对应的值。这也是 Redis 最基础的功能。

从 SET 命令开始探索

SET 命令的用法非常简单,那我们就从 SET 命令一路看下去,看看 SET 命令是如何实现的。

// 源码地址:https://github.com/redis/redis/blob/7.0.5/src/t_string.c#L284-L297
/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>]
*     [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] */
void setCommand(client *c) {
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = OBJ_NO_FLAGS;
    // 顾名思义,解析诸如 NX XX EX PX 等各种参数
    if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {
        return;
    }

    // 进行编码转换
    c->argv[2] = tryObjectEncoding(c->argv[2]);
    // SET 命令主要逻辑的实现
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

setCommand 函数依次调用了 parseExtendedStringArgumentsOrReplytryObjectEncodingsetGenericCommand 这三个函数。

其中 parseExtendedStringArgumentsOrReply 函数顾名思义,是用于解析 SET 命令的各种参数,如 NX XX 等,这不是 SET 命令的主要逻辑,不深究,继续往下看。

从命名看 tryObjectEncoding 函数似乎是对 c->argv[2] 进行编码转换,显然这也不是主要的逻辑。

至此,只剩下 setGenericCommand,从命名上看得出 setGenericCommand 应该是主要的函数逻辑,我们跳转到该函数:

// 源码地址:https://github.com/redis/redis/blob/7.0.5/src/t_string.c#L78-L141
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
    long long milliseconds = 0; /* initialized to avoid any harmness warning */
    int found = 0;
    int setkey_flags = 0;

    if (expire && ...) {
        /* 隐去和过期时间相关的处理逻辑 */
    }

    if (flags & OBJ_SET_GET) {
        /* 隐去和 flags 相关的处理逻辑 */
    }

    found = (lookupKeyWrite(c->db,key) != NULL);

    if ((flags & OBJ_SET_NX && found) ||
        (flags & OBJ_SET_XX && !found))
    {
        /* 隐去和 flags 相关的处理逻辑 */
    }

    // set 的主要逻辑
    setKey(c,c->db,key,val,setkey_flags);
    server.dirty++;
    notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);

    if (expire) {
        /* 隐去和过期时间相关的处理逻辑 */
    }

    if (!(flags & OBJ_SET_GET)) {
        /* 隐去和 flags 相关的处理逻辑 */
    }
}

隐去一些周边的处理逻辑后,我们能够明显地看到 setKey 是这个函数的主要逻辑,setKey 是将键值对存储在内存中的主要过程,看 setKey:

// 源码地址:https://github.com/redis/redis/blob/7.0.5/src/db.c#L261-L277
void setKey(client *c, redisDb *db, robj *key, robj *val, int flags) {
    int keyfound = 0;

    // 判断传入的 key 是否已经存在
    if (flags & SETKEY_ALREADY_EXIST)
        keyfound = 1;
    else if (!(flags & SETKEY_DOESNT_EXIST))
        keyfound = (lookupKeyWrite(db,key) != NULL);

    if (!keyfound) {
	// 如果 key 尚未存在,则调用 dbAdd 方法
        dbAdd(db,key,val);
    } else {
	// 如果 key 已经存在,则调用 dbOverwrtie 方法
        dbOverwrite(db,key,val);
    }
    // 使引用计数器自增
    incrRefCount(val);
    // 处理 key 过期相关问题
    if (!(flags & SETKEY_KEEPTTL)) removeExpire(db,key);
    if (!(flags & SETKEY_NO_SIGNAL)) signalModifiedKey(c,db,key);
}

在上面的函数中,我们可以看到主要逻辑是先判断 key 是否存在,根据 key 是否存在去调用 dbAdd 函数或 dbOverwrite 函数。这是 set 一个 key 的主要流程,后面对引用计数器的操作和对过期相关问题的处理,暂时忽略。

// 源码地址:https://github.com/redis/redis/blob/7.0.5/src/db.c#L188-L196
void dbAdd(redisDb *db, robj *key, robj *val) {
    sds copy = sdsdup(key->ptr);
    // 向 dist 中添加这个 key
    dictEntry *de = dictAddRaw(db->dict, copy, NULL);
    serverAssertWithInfo(NULL, key, de != NULL);
    // 向 dist 中增加对应 key 的 value
    dictSetVal(db->dict, de, val);
    // 告知客户端这个 key 的相关处理工作已经完成
    signalKeyAsReady(db, key, val->type);
    // cluster 相关,忽略
    if (server.cluster_enabled) slotToKeyAddEntry(de, db);
    // notify 相关,忽略
    notifyKeyspaceEvent(NOTIFY_NEW,"new",key,db->id);
}

dbAdd 这个函数主要调用了 dictAddRawdictSetVal,将相关的键值对存入 dict 中。看得出 dict 是字典 dictionary 的缩写,是 Redis 中存储键值对的数据结构。

总结

目前,我们一共看到了三个文件中的代码,根据其调用链,他们有这样的层次关系:t_string.c → db.c → dict.c。

不难发现,t_string.c 这个文件是用于处理 strings 这个数据结构相关的逻辑;db.c 是 Redis 主要的管理键值对的逻辑,对 key 的生命周期的管理主要存在于这个文件中;dict.c 是 Redis 中 HashTable 这个数据结构的实现。