
Redis 的指令表实现机制简介
所有组织良好的大型软件的一个共同特点,就是其模块化思维完备,换而言之,即使你是一个完全的新人,同样可以通过一个小模块以及其所链接的各种模块,很快地把项目熟悉起来。
而当我们把目光聚焦于 Redis 之上时,通过分析 SET
指令的实现,以及嵌入到 Redis 中的方式,我们自然而然地就可以对 Redis 的组织,架构等,建立相当的了解。
前置的一些知识
模块,实际上对应着一整套作业流程中的一个环节,而如果我们不能够理解某个工作环节的前置环节与后置环节,乃至于整体流程所服务的对象与工作的目的,我们就很难真正地做好我们手头的工作(“上下同欲者胜”)。
想要理解 Redis 的指令表实现机制,我们就需要了解,什么是指令?指令如何被组织?以及使用指令能够办成什么样的事情。
指令
指令便是用于指导 Redis 以展开某种行为的办法,往往需要结合一定的数据,如下面所示:
(向 Redis 中存储 "Hello world", 并将其提取出来)
而除了 GET
, SET
这两种基本的指令以外,Redis 也提供了 COMMAND
用以告知我们,Redis 所具有的所有的指令,以及这些指令的作用和使用他们的方法。
组织指令的办法
成熟的软件都有一种特点,即所有的模块,乃至于子程序,都有序地被摆放于他们应当所处的位置上,而 Redis 所设计的指令,都会在 commands
文件夹中,有一则记录,如图所示:
(可以看出,指令们被以 .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
指令为案例,参考如下:
而我们可以在 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;
}
(因为指令表中已经事先约定好所需要调用的函数,因此只需要按图索骥,用抽象化的函数指针调用对应函数,即可以执行指令)
Redis SET 指令的实现
Redis 的指令实现,伴随着版本的迭代,逐渐也增加了不少拓展的选项,如我们熟知的 SET
指令,现在不仅仅能够设置对应哈希表的数据,同时还可以指定过期的时间,乃至于可以获取被替换的数据(如果设置的键值已经存储了对应数据的话)。
而对应到 json 文件里面,就在于 arguments
参数,如图所示:
(这部分依旧是传统的 KEY, VALUE)
(这就有拓展的味道在里面了)
而对应到具体的代码之中,则体现为如下:
/* 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);
}
写在最后
欢迎更多的人参与到“数据库内核一周一审”的工作之中,一点一滴地把基本功扎实好,逐步建成一个繁荣的数据库内核生态,祝各位工作顺利,谢谢!