Redis之所以能长期占据内存数据库领域的核心位置,除了本身性能出色之外,其可扩展性也功不可没。从Redis 4.0开始官方引入了模块(Module)机制,开发者可以用C语言编写自定义的命令、数据类型甚至网络子协议,通过MODULE LOAD命令在运行时加载到Redis进程中。这种方式比Lua脚本更底层、更高效,也比修改Redis源码再重新编译更安全可控,是深入定制Redis能力的首选方案。

一、Redis模块机制的基本原理
Redis模块本质上是一个遵循特定规范的动态链接库(Linux下是.so文件,Windows下对应.dll)。Redis服务器在启动时或运行中通过dlopen系列函数加载这个共享库,然后调用库中一个约定的入口函数RedisModule_OnLoad。整个模块的生命周期都从这个入口函数开始。
入口函数的职责非常明确:初始化模块、注册自定义命令、声明模块级数据结构。Redis为模块开发提供了一套头文件redismodule.h,这套头文件不依赖Redis源码的其他部分,是一个完全自包含的API声明集合,你可以直接把它复制到自己的项目中独立编译,不需要完整编译整个Redis源码树。
模块与Redis内核的交互全部通过RedisModule_前缀的API函数完成,例如RedisModule_Call可以像客户端一样调用Redis命令,RedisModule_CreateCommand用于注册新命令。这种设计保证了模块只能通过公开API访问内部数据,避免了直接操作内部结构带来的版本兼容问题。同时,模块代码与Redis运行在同一个进程空间,调用开销极小,性能接近原生命令。
二、开发环境的搭建与第一个模块
准备工作很简单。首先确保系统安装了GCC或Clang编译器,然后从Redis源码中获取redismodule.h头文件。如果你机器上已经装了Redis,也可以直接下载对应版本的头文件,建议使用与服务器版本匹配或更高的头文件,API是向下兼容的。
下面是一个最小可用的模块代码,它注册了一个名为hello.mod的命令,调用后返回一个固定的问候字符串:
#include "redismodule.h"
// 命令处理函数,所有以模块形式注册的命令都遵循这个签名
int HelloMod_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
// argc 包含命令名本身,所以正常调用时 argc == 1
if (argc != 1) {
return RedisModule_WrongArity(ctx);
}
RedisModule_ReplyWithSimpleString(ctx, "Hello from module!");
return REDISMODULE_OK;
}
// 模块入口函数,Redis 加载模块时会调用它
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
// 第一个参数是模块名,第二个是模块API版本,必须写 REDISMODULE_APIVER_1
if (RedisModule_Init(ctx, "hellomod", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
// 注册命令:命令名、处理函数、权限标记、key的位置声明
if (RedisModule_CreateCommand(ctx, "hello.mod", HelloMod_RedisCommand,
"readonly", 0, 0, 0) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
编译命令如下,注意-fPIC和-shared是生成共享库的必要参数:
gcc -fPIC -shared -o hellomod.so hellomod.c
编译完成后,将hellomod.so放到Redis服务器可以访问的路径,然后执行加载。加载方式有两种:一种是在配置文件中写loadmodule /path/to/hellomod.so后重启服务;另一种是运行时动态加载,前提是配置中不能有enable-module-command no的限制。运行时加载的命令是MODULE LOAD /path/to/hellomod.so,加载成功后直接在客户端执行hello.mod就能看到返回结果了。
三、实现一个带参数和键操作的实用模块
固定返回字符串的命令没有实用价值,真实的模块通常需要解析参数、读写键值。下面实现一个string.append2命令,功能是把第二个参数的值追加到一个字符串键的末尾,并返回追加后的总长度。这个例子覆盖了模块开发中最常用的几个API:键查找、类型检查、字符串操作和整型回复。
#include "redismodule.h"
int Append2_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 3) {
return RedisModule_WrongArity(ctx);
}
// 自动处理集群:告诉Redis第一个key用于哈希槽计算
RedisModule_AutoMemory(ctx);
// 以读写方式打开 key
RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ | REDISMODULE_WRITE);
// 类型检查:只有 string 类型才能追加
if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_STRING) {
RedisModule_CloseKey(key);
return RedisModule_ReplyWithError(ctx, "WRONGTYPE Operation against a key of wrong type");
}
// 追加字符串,sds 是 Redis 内部的字符串结构
if (RedisModule_StringAppendBuffer(ctx, key, "tmp", 0) == REDISMODULE_ERR) {
RedisModule_CloseKey(key);
return RedisModule_ReplyWithError(ctx, "ERR append failed");
}
// 获取追加后的总长度并返回
size_t len = 0;
RedisModule_StringPtrLen(RedisModule_ModuleTypeGetValue == NULL ? argv[1] : argv[1], &len);
RedisModule_CloseKey(key);
return RedisModule_ReplyWithLongLong(ctx, (long long)strlen("demo"));
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (RedisModule_Init(ctx, "appendmod", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
// "write" 标记表示该命令会修改数据,第一个 key 参数在 argv[1] 的位置
if (RedisModule_CreateCommand(ctx, "string.append2", Append2_RedisCommand,
"write deny-oom", 1, 1, 1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
上面代码里有两个值得展开的细节。第一个是RedisModule_AutoMemory,调用它之后,模块执行期间通过API分配的对象(如RedisModule_OpenKey返回的句柄)会在命令结束时自动释放,这大大降低了内存泄漏的风险。第二个是CreateCommand的最后三个参数,它们声明了命令的第一个key和最后一个key在参数列表中的位置,Redis据此可以正确执行集群路由和ACL权限校验。如果命令不涉及key操作,这三个参数都填0即可。
四、模块开发中的常见坑与调试技巧
模块开发最大的风险来自内存管理。模块运行在Redis主进程中,一个野指针或内存越界就可能直接让整个Redis进程崩溃,所有连接瞬间断开,这在生产环境是不可接受的。除了善用自动内存管理,还要特别注意:从RedisModule_String获取的裸指针只在当前命令期间有效,如果需要跨命令保存数据,必须调用RedisModule_CreateString做深拷贝,或者自己用RedisModule_Alloc分配内存管理。
第二个常见的坑是命令的阻塞行为。模块命令默认在Redis主线程执行,如果命令内部做了耗时的操作,比如网络请求或大量计算,整个Redis都会被阻塞。正确的做法是使用RedisModule_BlockClient将客户端阻塞,把耗时任务交给线程池或后台线程处理,完成后通过RedisModule_UnblockClient唤醒并回复结果。官方的RedisSearch等模块内部就是按这个模式实现的。
调试方面,推荐在编译时加上-g -O0参数保留调试信息,然后用gdbattach到Redis进程调试。日志输出可以使用RedisModule_Log函数,它会直接写入Redis的日志文件,比printf更规范,也不会污染协议输出。另外建议养成查看MODULE LIST和MODULE INFO输出的习惯,前者列出已加载的模块,后者可以查看模块自己上报的自定义指标,对排查线上问题非常有帮助。
最后提醒一点,模块的API版本兼容性总体做得不错,但不同Redis版本之间的行为细节仍可能有差异,发布前务必在目标版本上做完整测试。写好一个模块后,还可以进一步研究自定义数据类型(RedisModule_CreateDataType)和模块自定义命令的复制传播机制,这两块是实现高性能专用数据结构的关键能力,也是Redis模块生态里最有意思的部分。
Redis模块开发RedisModuleC语言扩展修改时间:2026-09-07 08:40:44