青年数据库学习互助会

Redis 的指令表实现机制简介

文一,应急管理大学在读本科生,中国 PostgreSQL 分会实训基地秘书长,360 基础架构部 pikiwidb.com 设计者,PikiwiDB 与 IvorySQL 代码贡献者,MOP 社区会员,《数据库红皮书》(线上读物)译者,曾经在中国科学院,北京航空航天大学等地展开 PostgreSQL 内核相关的技术分享工作。

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

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

前置的一些知识

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

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

指令

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

Image

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

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

组织指令的办法

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

Image

Image

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

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


Redis commands README.md 中的有关信息

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

每一个 Json 文件都包含着指令本身的全部信息,但是他们并不能直接拿出来使用。所有希望了解指令信息的外部人员,都需要经由 COMMNAD INFO 和 COMMNAD 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 指令为案例,参考如下:

Image

而我们可以在 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;
}

Image

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

Redis SET 指令的实现

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

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

Image

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

Image

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

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

/* 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);
}

写在最后

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