Redis 启动时,第一步便是初始化服务器状态。在 main() 函数调用 initServer() 之前,会先执行 initServerConfig(),这个函数负责填充 server 结构体的大部分配置项,其中就包括命令表的初始化。命令表是 Redis 处理所有客户端请求的“总索引”,它的构建过程既体现了 Redis 对性能的极致追求,也展示了其模块化、自动化的工程思想。
一、initServerConfig() 中的准备工作
我们直接定位到 initServerConfig() 函数(位于 src/server.c)。在函数末尾,有这样几行关键代码:
c
/* Command table -- we initialize it here as it is part of the * initial configuration, since command names may be changed via * redis.conf using the rename-command directive. */ server.commands = dictCreate(&commandTableDictType); server.orig_commands = dictCreate(&commandTableDictType); populateCommandTable();
注释明确指出,命令表属于初始配置的一部分,因为命令名可能被 rename-command 配置修改,所以需要在加载配置文件之前(或之中)建立好原始表,并在之后根据配置进行重命名。
-
server.commands:当前生效的命令字典,查找命令时使用。 -
server.orig_commands:保留原始命令名的字典,不受rename-command影响,用于COMMAND命令等需要返回原始名称的场景。
这两个字典的类型都是 dict *,使用 dictCreate 并传入 &commandTableDictType,后者定义了字典的哈希函数、键比较函数等(位于 server.c 中)。字典的键是命令名字符串(sds),值是 struct redisCommand *。
二、populateCommandTable():将静态数组装入哈希表
populateCommandTable() 是构建命令表的核心。我们逐行剖析其源码:
c
void populateCommandTable(void) {
int j;
struct redisCommand *c;
for (j = 0;; j++) {
c = redisCommandTable + j; // ① 获取数组第 j 个元素的指针
if (c->declared_name == NULL) // ② 数组末尾哨兵,NULL 表示结束
break;
int retval1, retval2;
c->fullname = sdsnew(c->declared_name); // ③ 复制全名
if (populateCommandStructure(c) == C_ERR) // ④ 填充结构体内部细节
continue;
retval1 = dictAdd(server.commands, sdsdup(c->fullname), c);
retval2 = dictAdd(server.orig_commands, sdsdup(c->fullname), c);
serverAssert(retval1 == DICT_OK && retval2 == DICT_OK);
}
}
① redisCommandTable 是什么?
redisCommandTable 是一个全局静态数组,它在 commands.c 文件中定义(Redis 7.0 及以上版本由 utils/generate-command-code.py 脚本根据 src/commands/*.json 自动生成)。数组中每个元素都是一个 struct redisCommand 结构体,包含了命令的所有元数据:名称、处理函数指针、复杂度、历史版本、标志位、ACL 类别、键规格等。
例如,LPUSH 命令对应的条目可能是:
c
{"lpush", lpushCommand, -3, "wm", ...}
其中 lpushCommand 就是实际执行推送逻辑的函数。
② 结束条件
数组的最后一个元素使用 declared_name = NULL 作为哨兵,循环检测到 NULL 时退出。这避免了显式传递数组长度。
③ fullname 字段
declared_name 是命令的原始名称(如 "lpush"),fullname 同样保存该名称,但在子命令场景(如 CLUSTER INFO)中,fullname 可能是 "cluster|info" 这样的复合形式,用于唯一标识。
④ populateCommandStructure(c):精细填充
这个函数(也在 server.c)非常重要,它负责:
-
解析
c->flags中的位标志,设置快速路径(如CMD_FAST)。 -
构建子命令字典:如果命令有子命令(如
CLUSTER),它会递归调用populateCommandTable填充subcommands_dict。 -
处理键规格(key specs):根据
key_specs和get_keys函数设置键解析逻辑,用于集群重定向和键空间事件。
只有 populateCommandStructure 返回 C_OK,该命令才会被插入字典,否则跳过。
⑤ 插入字典
使用 sdsdup(c->fullname) 复制键名,避免共享同一字符串导致的潜在问题。dictAdd 将键值对存入两个字典。serverAssert 确保插入成功(不会出现重复键)。
三、redisCommandTable 的来源与结构
现代 Redis 采用代码生成技术,在编译时根据 JSON 定义生成 commands.c。这样做的好处是:
-
单一数据源:命令文档、历史、参数都在 JSON 中维护,避免多处修改不同步。
-
易于扩展:新增命令只需添加 JSON 文件,重新编译即可。
-
减少人工错误:自动生成的数组结构完全一致。
即使不是自动生成,用户看到的 redisCommandTable 也是以 MAKE_CMD 宏包裹的,宏展开后会形成合法的 C 结构体数组。最终,该数组直接暴露给 populateCommandTable 使用。
四、为什么不用数组直接查找?
既然 redisCommandTable 已经是完整的数组,为什么不直接遍历数组进行命令匹配?答案显而易见:性能。Redis 是单线程事件循环,每个命令请求都必须快速响应。如果每次都用线性搜索(甚至二分查找),在命令多达数百条时也会拖慢速度。而字典(哈希表)查找是 O(1) 的,无论命令数量多少,耗时基本恒定。
另外,rename-command 功能允许管理员修改命令名,如果直接修改数组会破坏原始定义,因此使用字典单独存储映射关系,既保留了原始表,又实现了灵活的别名。
五、初始化时机与后续影响
populateCommandTable() 在 initServerConfig() 中执行,早于加载 redis.conf。这意味着:
-
server.commands最初包含了所有命令的原始名称。 -
如果配置文件中出现
rename-command指令,Redis 会调用changeCommandName()函数,从server.commands中删除旧键,添加新键。而server.orig_commands保持不变,所以COMMAND命令依然能返回真实名称。
这样做既满足了安全性(隐藏危险命令),又不丢失元数据。
六、总结
通过 initServerConfig() 中的三步曲:
-
创建两个命令字典;
-
调用
populateCommandTable(); -
遍历自动生成的
redisCommandTable,填充结构体并将其插入字典。
Redis 将静态的命令元数据转化为高效运行时查找的数据结构,为后续每条命令的快速定位奠定了坚实基础。这一设计体现了数据库系统常见的“启动时构建索引,运行时高速查找”理念,也展示了 Redis 在保持代码可维护性的同时,对性能毫不妥协的工程态度。
当你下次键入 LPUSH 时,可以想象,在那一瞬间,Redis 只是从一个哈希表中取出了预置好的函数指针,无需任何解析或搜索,直接跳入处理函数——这一切,都源于服务器启动时那看似平凡却精巧的“命令数组构建过程”。

5551

被折叠的 条评论
为什么被折叠?



