Redis 的指令表实现机制简介

Redis 的指令表实现机制简介

文一

2024-09-17 发布73 浏览 · 0 点赞 · 0 收藏

所有组织良好的大型软件的一个共同特点,就是其模块化思维完备,换而言之,即使你是一个完全的新人,同样可以通过一个小模块以及其所链接的各种模块,很快地把项目熟悉起来。

而当我们把目光聚焦于 Redis 之上时,通过分析 SET 指令的实现,以及嵌入到 Redis 中的方式,我们自然而然地就可以对 Redis 的组织,架构等,建立相当的了解。

前置的一些知识

模块,实际上对应着一整套作业流程中的一个环节,而如果我们不能够理解某个工作环节的前置环节与后置环节,乃至于整体流程所服务的对象与工作的目的,我们就很难真正地做好我们手头的工作(“上下同欲者胜”)。

想要理解 Redis 的指令表实现机制,我们就需要了解,什么是指令?指令如何被组织?以及使用指令能够办成什么样的事情。

指令

指令便是用于指导 Redis 以展开某种行为的办法,往往需要结合一定的数据,如下面所示:

example
(向 Redis 中存储 "Hello world", 并将其提取出来)

而除了 GET, SET 这两种基本的指令以外,Redis 也提供了 COMMAND 用以告知我们,Redis 所具有的所有的指令,以及这些指令的作用和使用他们的方法。

组织指令的办法

成熟的软件都有一种特点,即所有的模块,乃至于子程序,都有序地被摆放于他们应当所处的位置上,而 Redis 所设计的指令,都会在 commands 文件夹中,有一则记录,如图所示:

redis-src-command

redis-src-command-2

(可以看出,指令们被以 .json 的方式加以记录)

而放置于文件夹中的 README.md,则告诉了我们相关的背景信息:


Redis commands README.md 中的有关信息

本文件夹包含着一些 json 文件,每一个 json 文件都对应着一条 Redis 的指令。

每一个 Json 文件都包含着指令本身的全部信息,但是他们并不能直接拿出来使用。所有希望了解指令信息的外部人员,都需要经由 COMMNAD INFOCOMMNAD DOCS 两则指令来进行了解(译者注:两者反馈的信息大体一致,但从细节上看,差异还是有,从我们的使用经验看,推荐读者使用 COMMAND DOCS,因为它组织文档信息的方式更合理)。

这些 Json 文件被用于生成 commnads.def 文件,它的组织形式就类似于直接使用 COMMAND 来输出的结果,但是因为一些域以及标志位是隐式填入,因此我们并不能依靠这些原始文件,来推导最终的输出结果(译者注:这就像 PostgreSQL 输出信息所使用的语言,往往需要结合具体的场景加以分析一样)。

Json 文件的组织方式被放置于 https://redis.io/commands/command-docs/ 与 https://redis.io/commands/command/


有了这个理解之后,我们可以继续阅读 Json 文件组织方式的有关文档,出于简化的目的,我们只需要了解如下的关键信息即可:

  • group 在 Redis 中,他们被用于为指令划分类别,比如在 SET 指令中,"string" 的类别代表它主要是为字符串数据而服务
  • arity 用以描述这条指令接收多少参数,倘若为正数,代表只接收对应数目的参数,反之代表接收参数数目是可变的
  • function 用于实现这条指令的C语言函数
  • get_keys_function 用于传递参数给指令实现函数的C语言函数
  • arguments 指令所需要的参数
  • reply_schema 指令执行完成后,所反馈给用户的内容

使用 Redis 指令可以完成什么样的工作?

取决于你使用了怎样的指令,以 COMMAND 指令为案例,参考如下:

command-json

而我们可以在 Redis 中直接找到 commandCommand 函数的实现,参考如下:

/* COMMAND (no args) */
void commandCommand(client *c) {
    /* Redis 的命令表存储于动态哈希表之中 */
    dictIterator *di;
    dictEntry *de;

    /* 根据指令表的长度,增加反馈到客户端字符串的长度 */
    addReplyArrayLen(c, dictSize(server.commands));

    /* 遍历指令表,逐指令构造反馈给用户的内容 */
    di = dictGetIterator(server.commands);
    while ((de = dictNext(di)) != NULL) {
        addReplyCommandInfo(c, dictGetVal(de));
    }
    /* 完成遍历,释放指针 */
    dictReleaseIterator(di);
}

而在实际的工作之中, Redis 统一抽象化了执行指令的流程至 processCommand 函数之中, 参考如下:

/*
  用于执行指令的函数, 传递进入的参数为客户端的 argc, argv 数据
  C_OK 在客户端正常工作且其它操作能够被调用者执行时返回
  否则 C_ERR 就会被返回去
*/
int processCommand(client *c) {
    /* 避免与 Lua 脚本同时执行,造成数据的不一致性 */
    if (!scriptIsTimedout()) {
        /* ... */
    }

    /* 判断指令与数据是否已经是之前执行过的,如果是,则可以跳过一些步骤,节约时间 */
    int client_reprocessing_command = c->cmd ? 1 : 0;
  
    if (!client_reprocessing_command) {
        moduleCallCommandFilters(c);
        reqresAppendRequest(c);
    }

    /* ... */

    /* Exec the command */
    if (c->flags & CLIENT_MULTI &&
        c->cmd->proc != execCommand &&
        c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand &&
        c->cmd->proc != watchCommand &&
        c->cmd->proc != quitCommand &&
        c->cmd->proc != resetCommand)
    {
        /* 如果处于事务状态,则走这部分流程*/
        queueMultiCommand(c, cmd_flags);
        addReply(c,shared.queued);
    } else {
        int flags = CMD_CALL_FULL;
        if (client_reprocessing_command) flags |= CMD_CALL_REPROCESSING;
        /* 执行指令表中所对应的函数 */
        call(c,flags);
        if (listLength(server.ready_keys) && !isInsideYieldingLongCommand())
            handleClientsBlockedOnKeys();
    }
    return C_OK;
}

exec-proc

(因为指令表中已经事先约定好所需要调用的函数,因此只需要按图索骥,用抽象化的函数指针调用对应函数,即可以执行指令)

Redis SET 指令的实现

Redis 的指令实现,伴随着版本的迭代,逐渐也增加了不少拓展的选项,如我们熟知的 SET 指令,现在不仅仅能够设置对应哈希表的数据,同时还可以指定过期的时间,乃至于可以获取被替换的数据(如果设置的键值已经存储了对应数据的话)。

而对应到 json 文件里面,就在于 arguments 参数,如图所示:

set-json

(这部分依旧是传统的 KEY, VALUE)

set-extend

(这就有拓展的味道在里面了)

而对应到具体的代码之中,则体现为如下:

/* 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; /* 对应的 Flags */

    /* 解析参数,实际上拓展的参数看上去多,但实际上对应的数据,无非是过期的时间,乃至于是否返回被替代的内容几类,做一个分支判断,即可以解决 */
    if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {
        return;
    }

    c->argv[2] = tryObjectEncoding(c->argv[2]);
    /* 做一个普遍性的设置即可完成 */
    /* 这部分算法的核心,在于要对哈希表有理解 */
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

写在最后

欢迎更多的人参与到“数据库内核一周一审”的工作之中,一点一滴地把基本功扎实好,逐步建成一个繁荣的数据库内核生态,祝各位工作顺利,谢谢!