好的,咱们今天来聊聊 Redis Modules,也就是用 C/C++ 给 Redis 添砖加瓦,让它干更多“不正经”的事儿。准备好了吗?要开车了!
开场白:Redis 的“野心”与 Module 的诞生
各位,Redis 大家都熟吧?快,稳,狠,数据结构丰富,用起来贼爽。但是,人总是贪心的嘛,有了女朋友还想找个……咳咳,跑题了。我是说,有了 Redis 这么好用的东西,我们还想让它做更多的事情。
比如说,你想让 Redis 支持一种全新的数据结构,比如“图”(Graph),或者你想让 Redis 集成一个牛逼的搜索引擎,或者你想让 Redis 直接连上你的 AI 模型搞事情。
这时候,光靠 Redis 自带的命令和数据结构就有点力不从心了。咋办?难道要魔改 Redis 源码?NO NO NO,太危险了!万一改崩了,就等着老板给你穿小鞋吧。
所以,Redis 的开发者们很聪明,搞了个叫做 Redis Modules 的东西。它允许你用 C/C++ 编写扩展模块,像插件一样插到 Redis 里,增强 Redis 的功能。
Module 的优势:为什么我们要用它?
- 性能至上: C/C++ 写的代码,性能杠杠的,比 Lua 脚本快多了(当然,也比 Lua 难写多了)。
- 自由度高: 想怎么折腾就怎么折腾,只要你的 C/C++ 够硬,你可以让 Redis 上天入地。
- 生态丰富: 已经有很多优秀的 Redis Modules 诞生了,比如 RediSearch(全文搜索)、RedisBloom(布隆过滤器)等等,拿来就能用,省时省力。
- 安全可靠: Module 运行在独立的沙箱里,不会轻易搞崩 Redis 主进程。
Module 的结构:麻雀虽小,五脏俱全
一个 Redis Module,其实就是一个动态链接库(.so 文件),里面包含了一些 Redis 规定的入口函数。这些入口函数就像是 Module 的“接口”,Redis 通过这些接口来调用 Module 的功能。
一个最简单的 Module,至少需要一个 RedisModule_OnLoad
函数,这个函数会在 Module 加载的时候被调用,用来注册命令、数据类型、钩子等等。
开发环境搭建:磨刀不误砍柴工
要开发 Redis Module,你需要先准备好以下东西:
- Redis 源码: 不是让你去改源码,而是需要里面的
redismodule.h
头文件,这个头文件定义了 Module API。 - C/C++ 编译器: GCC 或者 Clang 都可以。
- Make 工具: 用来编译 Module。
- Linux 环境: 虽然理论上 Windows 也能开发,但是推荐在 Linux 下搞,省事儿。
第一个 Module:Hello World!
咱们先来写一个最简单的 Module,让它注册一个 hello.world
命令,执行后返回 "Hello World!"。
- 创建
hello.c
文件:
#include <redismodule.h>
int HelloWorld_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
// 检查参数个数
if (argc != 1) {
return RedisModule_WrongArity(ctx);
}
// 回复 "Hello World!"
RedisModule_ReplyWithString(ctx, RedisModule_CreateString(ctx, "Hello World!", 12));
return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
// 检查 API 版本
if (RedisModule_Init(ctx, "hello", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
// 注册命令
if (RedisModule_CreateCommand(ctx, "hello.world", HelloWorld_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
- 编写
Makefile
文件:
MODULE_NAME = hello
REDIS_MODULE_H = /path/to/redis/src/redismodule.h # 替换成你 redismodule.h 的实际路径
all: $(MODULE_NAME).so
$(MODULE_NAME).so: hello.c
gcc -fPIC -shared -o $@ $< -I$(REDIS_MODULE_H)
clean:
rm -f $(MODULE_NAME).so
注意: 把 REDIS_MODULE_H
替换成你 redismodule.h
文件的实际路径。
- 编译 Module:
在命令行执行 make
命令。
- 加载 Module:
启动 Redis 服务器,然后执行:
redis-cli MODULE LOAD ./hello.so
- 测试 Module:
redis-cli hello.world
如果一切顺利,你应该会看到 "Hello World!"。
代码解释:
RedisModule_OnLoad
:Module 加载时执行的函数,用来初始化 Module,注册命令等等。RedisModule_Init
:初始化 Module,指定 Module 的名字、版本和 API 版本。RedisModule_CreateCommand
:注册命令,指定命令的名字、处理函数、flags 等等。HelloWorld_RedisCommand
:命令的处理函数,接收 Redis 传过来的参数,处理完后返回结果。RedisModuleCtx
:Module 上下文,包含了 Redis 提供的一些 API 函数。RedisModuleString
:Redis 的字符串类型,类似于sds
。RedisModule_ReplyWithString
:回复字符串给客户端。RedisModule_CreateString
:创建一个 Redis 字符串。REDISMODULE_OK
:表示成功。REDISMODULE_ERR
:表示失败。RedisModule_WrongArity
:当命令参数个数错误时调用
深入 Module API:RedisModule_API
Redis 提供了大量的 API 函数,让你可以访问 Redis 的各种功能。这些 API 函数都定义在 redismodule.h
头文件里,你可以查阅 Redis 官方文档来了解它们的用法。
下面是一些常用的 API 函数:
API 函数 | 功能 |
---|---|
RedisModule_CreateCommand |
注册命令。 |
RedisModule_Call |
调用 Redis 命令。 |
RedisModule_OpenKey |
打开一个 Redis key,用于读取或写入。 |
RedisModule_CloseKey |
关闭一个 Redis key。 |
RedisModule_GetValue |
获取 key 的值。 |
RedisModule_SetValue |
设置 key 的值。 |
RedisModule_ReplyWithLongLong |
回复一个整数给客户端。 |
RedisModule_ReplyWithString |
回复一个字符串给客户端。 |
RedisModule_ReplyWithArray |
回复一个数组给客户端。 |
RedisModule_CreateStringFromString |
从 C 字符串创建一个 Redis 字符串。 |
RedisModule_StringPtrLen |
获取 Redis 字符串的指针和长度。 |
RedisModule_FreeString |
释放 Redis 字符串。 |
RedisModule_Log |
记录日志。 |
RedisModule_CreateDict |
创建一个字典。 |
RedisModule_DictAdd |
向字典中添加一个键值对。 |
RedisModule_DictGet |
从字典中获取一个值。 |
RedisModule_FreeDict |
释放字典。 |
RedisModule_Alloc |
分配内存。 |
RedisModule_Free |
释放内存。 |
RedisModule_GetContextFlags |
获取 Module 的上下文标志,可以用来判断 Module 是否运行在 RDB 加载或 AOF 重写过程中。 |
RedisModule_SetModuleOptions |
设置 Module 的选项,比如设置 Module 的内存分配器。 |
RedisModule_SubscribeToServerEvent |
订阅 Redis 服务器事件,比如 key 过期、key 被删除等等。 |
RedisModule_UnsubscribeFromServerEvent |
取消订阅 Redis 服务器事件。 |
RedisModule_GetClientId |
获取客户端 ID。 |
RedisModule_GetClientInfo |
获取客户端信息,比如客户端的 IP 地址、端口号等等。 |
RedisModule_CreateDataType |
注册自定义数据类型。 |
RedisModule_SaveUnlinkedValue |
保存一个未链接的值到 RDB 文件,用于自定义数据类型的持久化。 |
RedisModule_LoadUnlinkedValue |
从 RDB 文件加载一个未链接的值,用于自定义数据类型的持久化。 |
RedisModule_ReplicateVerbatim |
复制命令到 Slave 节点,用于实现主从复制。 |
RedisModule_GetBlockedClientPrivateData |
获取阻塞客户端的私有数据,用于实现阻塞命令。 |
RedisModule_SetBlockedClientPrivateData |
设置阻塞客户端的私有数据,用于实现阻塞命令。 |
RedisModule_AbortBlock |
中止阻塞客户端的阻塞状态,用于实现阻塞命令。 |
RedisModule_IsBlockedReplyTimedout |
检查阻塞客户端的回复是否超时,用于实现阻塞命令。 |
RedisModule_GetExpire |
获取 key 的过期时间。 |
RedisModule_SetExpire |
设置 key 的过期时间。 |
RedisModule_UnblockClient |
解除客户端的阻塞。 |
RedisModule_BlockedClientTryLock |
尝试锁定一个阻塞的客户端。 |
RedisModule_BlockedClientUnlock |
解锁一个阻塞的客户端。 |
RedisModule_BlockedClientSetValue |
设置阻塞客户端的值。 |
RedisModule_BlockedClientSetAuxData |
设置阻塞客户端的辅助数据。 |
RedisModule_BlockedClientGetAuxData |
获取阻塞客户端的辅助数据。 |
RedisModule_BlockedClientMarkReadyForReply |
标记阻塞客户端准备好回复。 |
RedisModule_BlockedClientReply |
回复阻塞客户端。 |
自定义数据类型:让 Redis 存储你的专属数据
Redis 自带的数据类型虽然丰富,但总有满足不了你的时候。比如,你想让 Redis 存储复杂的对象,或者你想实现一种全新的数据结构。
这时候,你就可以使用 Redis Module 的自定义数据类型功能。
- 定义数据类型结构体:
typedef struct {
int id;
char name[64];
} MyObjectType;
- 定义数据类型的 API:
你需要实现一些 API 函数,用来创建、复制、释放、持久化你的数据类型。
// 创建函数
void *MyObjectType_Create(void) {
MyObjectType *obj = RedisModule_Alloc(sizeof(MyObjectType));
obj->id = 0;
strcpy(obj->name, "default");
return obj;
}
// 复制函数
void *MyObjectType_Duplicate(void *value) {
MyObjectType *src = (MyObjectType *)value;
MyObjectType *dst = RedisModule_Alloc(sizeof(MyObjectType));
dst->id = src->id;
strcpy(dst->name, src->name);
return dst;
}
// 释放函数
void MyObjectType_Free(void *value) {
RedisModule_Free(value);
}
// RDB 保存函数
void MyObjectType_RdbSave(RedisModuleIO *io, void *value) {
MyObjectType *obj = (MyObjectType *)value;
RedisModule_SaveLongLong(io, obj->id);
RedisModule_SaveString(io, RedisModule_CreateString(NULL, obj->name, strlen(obj->name)));
}
// AOF 重写函数
void MyObjectType_AofRewrite(RedisModuleIO *io, RedisModuleString *key, void *value) {
MyObjectType *obj = (MyObjectType *)value;
RedisModule_AofPrintf(io, "MYOBJECT.SET %s %lld %srn",
RedisModule_StringPtrLen(key, NULL), obj->id, obj->name);
}
// RDB 加载函数
void *MyObjectType_RdbLoad(RedisModuleIO *io) {
MyObjectType *obj = RedisModule_Alloc(sizeof(MyObjectType));
obj->id = RedisModule_LoadLongLong(io);
RedisModuleString *name = RedisModule_LoadString(io);
strcpy(obj->name, RedisModule_StringPtrLen(name, NULL));
RedisModule_FreeString(name);
return obj;
}
// 比较函数
int MyObjectType_Compare(void *a, void *b) {
MyObjectType *oa = (MyObjectType *)a;
MyObjectType *ob = (MyObjectType *)b;
return oa->id - ob->id;
}
// Digest 函数
void MyObjectType_Digest(RedisModuleDigest *md, void *value) {
MyObjectType *obj = (MyObjectType *)value;
RedisModule_DigestAddLongLong(md, obj->id);
RedisModule_DigestAddStringBuffer(md, obj->name, strlen(obj->name));
}
- 注册数据类型:
在 RedisModule_OnLoad
函数中注册你的数据类型。
RedisModuleType *MyObjectType_RedisModuleType;
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (RedisModule_Init(ctx, "myobject", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
RedisModuleTypeMethods tm = {
.version = REDISMODULE_TYPE_METHOD_VERSION,
.rdb_load = MyObjectType_RdbLoad,
.rdb_save = MyObjectType_RdbSave,
.aof_rewrite = MyObjectType_AofRewrite,
.free = MyObjectType_Free,
.digest = MyObjectType_Digest,
.compare = MyObjectType_Compare,
.duplicate = MyObjectType_Duplicate
};
MyObjectType_RedisModuleType = RedisModule_CreateDataType(ctx, "myobject", 0, &tm);
if (MyObjectType_RedisModuleType == NULL) {
return REDISMODULE_ERR;
}
// 注册命令,用来操作 MyObjectType
if (RedisModule_CreateCommand(ctx, "myobject.set", MyObject_SetCommand, "write", 1, 1, 1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
if (RedisModule_CreateCommand(ctx, "myobject.get", MyObject_GetCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
- 编写命令处理函数:
编写命令处理函数,用来操作你的自定义数据类型。
int MyObject_SetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 4) {
return RedisModule_WrongArity(ctx);
}
RedisModuleString *key_name = argv[1];
long long id;
if (RedisModule_StringToLongLong(argv[2], &id) != REDISMODULE_OK) {
return RedisModule_ReplyWithError(ctx, "Invalid id");
}
const char *name = RedisModule_StringPtrLen(argv[3], NULL);
// 打开 key
RedisModuleKey *key = RedisModule_OpenKey(ctx, key_name, REDISMODULE_WRITE);
// 检查 key 的类型
if (RedisModule_KeyType(key) != REDISMODULE_KEYTYPE_EMPTY &&
RedisModule_KeyType(key) != RedisModule_KeyType(key)) {
RedisModule_CloseKey(key);
return RedisModule_ReplyWithError(ctx, "Wrong key type");
}
// 创建 MyObjectType 对象
MyObjectType *obj = MyObjectType_Create();
obj->id = id;
strcpy(obj->name, name);
// 设置 key 的值
RedisModule_SetValue(key, MyObjectType_RedisModuleType, obj);
// 关闭 key
RedisModule_CloseKey(key);
RedisModule_ReplyWithSimpleString(ctx, "OK");
return REDISMODULE_OK;
}
int MyObject_GetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 2) {
return RedisModule_WrongArity(ctx);
}
RedisModuleString *key_name = argv[1];
// 打开 key
RedisModuleKey *key = RedisModule_OpenKey(ctx, key_name, REDISMODULE_READ);
// 检查 key 的类型
if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) {
RedisModule_CloseKey(key);
return RedisModule_ReplyWithError(ctx, "Key not found");
}
if (RedisModule_KeyType(key) != RedisModule_KeyType(key)) {
RedisModule_CloseKey(key);
return RedisModule_ReplyWithError(ctx, "Wrong key type");
}
// 获取 MyObjectType 对象
MyObjectType *obj = RedisModule_GetValue(key);
// 回复客户端
RedisModule_ReplyWithArray(ctx, 2);
RedisModule_ReplyWithLongLong(ctx, obj->id);
RedisModule_ReplyWithString(ctx, RedisModule_CreateString(ctx, obj->name, strlen(obj->name)));
// 关闭 key
RedisModule_CloseKey(key);
return REDISMODULE_OK;
}
事件通知:监听 Redis 的风吹草动
Redis Module 还可以订阅 Redis 服务器的事件,比如 key 过期、key 被删除等等。这样,你就可以在这些事件发生时执行一些自定义的操作。
- 订阅事件:
在 RedisModule_OnLoad
函数中订阅事件。
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
// ...
if (RedisModule_SubscribeToServerEvent(ctx, REDISMODULE_EVENT_KEYSPACE, "del", MyKeyDelCallback) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
- 编写事件回调函数:
编写事件回调函数,用来处理事件。
void MyKeyDelCallback(RedisModuleCtx *ctx, int event_type, const char *event, RedisModuleString *key) {
// key 被删除时执行的操作
RedisModule_Log(ctx, "warning", "Key '%s' was deleted!", RedisModule_StringPtrLen(key, NULL));
}
阻塞命令:让 Redis 等你一下
有时候,你需要让 Redis 等待某个条件满足才能返回结果。比如,你想实现一个消息队列,当队列为空时,让客户端阻塞等待新的消息。
这时候,你就可以使用 Redis Module 的阻塞命令功能。
主从复制:让你的 Module 在集群中飞翔
如果你想让你的 Module 在 Redis 集群中正常工作,你需要考虑主从复制的问题。你需要确保你的 Module 的数据和状态能够正确地复制到 Slave 节点。
性能优化:让你的 Module 跑得更快
- 减少内存分配: 尽量使用 Redis 提供的字符串 API,避免频繁的内存分配和释放。
- 使用缓存: 缓存一些常用的数据,避免重复计算。
- 避免阻塞: 尽量避免在命令处理函数中执行耗时的操作,可以使用异步任务来处理。
调试技巧:让你的 Module 摆脱 Bug 的困扰
- 使用
RedisModule_Log
: 记录日志,方便调试。 - 使用 GDB: 调试 C/C++ 代码。
- 使用 Valgrind: 检测内存泄漏和非法访问。
总结:Redis Module 的无限可能
Redis Module 为我们提供了一个强大的工具,可以用来扩展 Redis 的功能,让 Redis 变得更加灵活和强大。只要你敢想,敢做,你就可以用 Redis Module 实现各种各样的奇葩功能。
好了,今天的讲座就到这里,希望对大家有所帮助。如果大家有什么问题,欢迎提问。
友情提示:
开发 Redis Module 是一项很有挑战性的工作,需要你具备扎实的 C/C++ 基础和对 Redis 内部机制的理解。但是,只要你坚持学习,不断实践,你一定能够成为一名优秀的 Redis Module 开发者。
祝大家编程愉快!