我已经使用 Redis 大约六年了,多年来,我利用其少数几个数据类型做了一些非常多功能的事情,例如地理查询、文本搜索、机器学习等。
但是,多年来,我无法计算我已经有多少次(以及许多其他用户)希望 Redis 增加一些它所缺少的数据结构、命令或功能。因为没有它们,用户不得不诉诸不太优雅或不太高效的方法来实现我们在 Redis 中的目标。虽然 Lua 朝着正确的方向迈进了一步,但它也有自己的局限性。
但这些都正在发生改变,因为 Redis 现在拥有一个(尚未稳定) 的模块系统,它允许开发人员编写 C 库,以向 Redis 中添加新的功能和数据结构 - 其速度与普通的 Redis 命令相似。
我个人认为这是在很长一段时间里影响 Redis 最重要的一个新功能,它将超越 API - 在未来几年里显著影响 Redis 的动态和使用方式。
在过去几个月里,萨尔瓦托雷一直在编写一个相当大的模块 API,而我们 Redis 一直在忙于对此进行测试,创建一些模块来添加诸如认证、概率性数据结构和全文搜索等功能 - 所有这些都使用了新的数据结构,并在 Redis 中添加了新命令。
因此,我想分享一下我学到的东西,作为编写模块的一个基本指南。API 文档非常好,但也非常长。因此,请将本指南作为模块编写的 TL;DR 指南。
让我们从基础知识开始:模块基本上是 C 共享库(.so 文件),Redis 可以在运行时或启动时加载它们。它们可以注册 Redis 不支持的新命令,并且可以访问 Redis 的数据来执行其操作。但有些关键的区别让模块变得更加强大
模块基本上包含的都是命令处理程序。这些是具有以下签名的 C 函数
int MyCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
此理念非常简单,从标识可见:该函数返回一个整数,可能是 OK 或 ERR。通常,即使向用户返回错误,返回 OK 也是可以的。
命令处理程序接受一个 RedisModuleCtx* 对象。此对象对于模块开发人员来说是不透明的,但其内部包含调用客户机状态,甚至我们稍后会用到的内部内存管理。
接下来,它接收 argv 和 argc,它们基本上是用户已传递给被调用命令的参数。第一个参数是调用的名称本身,其余的只是从 Redis 协议中解析而来的参数。
请注意,它们以 RedisModuleString 对象接收,它们同样是不透明的。如果需要操作,它们可以以零拷贝转换为常规 C 字符串。
要激活模块的命令,模块的标准入口点是一个名为 RedisModule_OnLoad 的函数。此函数告知 Redis 模块中有哪些命令并将它们映射到各自的处理程序。
好的,让我们编写一个 Redis 模块。我们将重点关注实现一个新的 Redis 命令的非常简单的模块示例 - HGETSET <key> <element> <new value>。它基本上是 HGET 和 HSET 的组合,获取散列对象中的当前值,并将新值原子地设置为它的新值。这相当基础,可以使用一个简单的事务或 Lua 脚本来完成,但它的优点在于非常简单。
1. 让我们从一个裸命令处理程序开始
int HGetSetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
return REDISMODULE_OK;
}
同样,此命令当前没有任何作用,它只是返回 OK 代码。因此,让我们给它赋予一些实质内容。
2. 验证参数
请记住,我们的命令是 HGETSET <key> <element> <new value>,这意味着 argv 中将始终有四个参数。因此,我们来确保确实发生了这种情况
/**
HGETSET <key> <element> <new value>
*/
int HGetSetCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (argc != 4) {
return RedisModule_WrongArity(ctx);
}
return REDISMODULE_OK;
}
RedisModule_WrongArity 将以下形式向客户端返回标准错误
(error) ERR 'get' 命令的参数数目错误
3. 激活 AutoMemory
Redis 模块 API 的一个重要特性是自动资源和内存管理。虽然模块作者可以独立分配与释放内存,但 Redis 资源的创建受到管理,而处理程序的生命周期内分配的 Redis 字符串、键和响应如果调用 RedisModule_AutoMemory,将自动释放。
RedisModule_AutoMemory(ctx);
4. 执行 Redis 调用
现在,我们将运行两个 Redis 调用中的第一个:HGET。我们传递 argv[1] 和 argv[2],它们分别是键和元素,作为参数。我们使用通用的 RedisModule_Call 命令,它允许模块开发人员仅仅像 Lua 脚本一样调用任何现有的 Redis 命令。
RedisModuleCallReply *rep =
RedisModule_Call(ctx, “HGET”, “ss”, argv[1], argv[2]);
// 让我们确保它不出错
if (RedisModule_CallReplyType(rep) == REDISMODULE_REPLY_ERROR) {
RedisModule_ReplyWithCallReply(ctx, rep);
return REDISMODULE_ERR;
}
请注意,RedisModule_Call 的第三个参数 “ss” 表示 Redis 应该如何处理函数的传递的多变参数。“ss” 表示“两个 RedisModuleString 对象”。其他说明符包括,表示 c 字符串的 “c”,表示双精度的 “d”,表示长的 “l”,表示 c 缓冲区(字符串后跟其长度)的 “b”。
现在让我们执行第二个 Redis 调用,HSET
RedisModuleCallReply *srep =
RedisModule_Call(ctx, “HSET”, “sss”, argv[1], argv[2], argv[3]);
if (RedisModule_CallReplyType(srep) == REDISMODULE_REPLY_ERROR) {
RedisModule_ReplyWithCallReply(ctx, srep);
return REDISMODULE_ERR;
}
这与 HGET 命令非常相似,只不过我们向它传递了三个参数。
5. 返回结果
在这个简单的示例中,我们只需要返回 HGET 的结果,或更改前的值。这通过一个简单的函数完成 – RedisModule_ReplyWithCallReply,它将回复对象转发给客户端
RedisModule_ReplyWithCallReply(ctx, rep);
return REDISMODULE_OK;
就这么简单!我们的命令处理程序已经准备就绪,我们只需要正确注册我们的模块和命令处理程序。
6. 初始化模块
所有 Redis 模块的入口点是 RedisModule_OnLoad 函数,开发人员必须实现此函数。它注册并初始化模块,并将其命令注册到 Redis,以便可以调用这些命令。
初始化我们的模块的任务如下
int RedisModule_OnLoad(RedisModuleCtx *ctx) {
// 注册模块本身 – 它称为 example,具有 API 版本 1
if (RedisModule_Init(ctx, “example”, 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
// 注册我们的命令——它是一个写入命令,其中有一个 argv[1] 键值
if (RedisModule_CreateCommand(ctx, “example.hgetset”, HGetSetCommand,
“write”, 1, 1, 1) == REDISMODULE_ERR) {
return REDISMODULE_ERR;
}
return REDISMODULE_OK;
}
差不多了!我们的模块完成了。
7. 有关模块构建的一个词
剩下的就是编译我们的模块。我不会详细介绍如何为它创建 makefile,但你需要知道的是:Redis 模块不需要任何特殊链接。一旦你在模块的文件中包含了 “redismodule.h” 文件并实现了入口点函数,Redis 就会加载你的模块。所有其他链接都取决于你。
此处提供了使用 gcc 编译我们基本模块所需的命令。
在 Linux 中
$ gcc -fPIC -std=gnu99 -c -o module.o module.c
$ ld -o module.so module.o -shared -Bsymbolic -lc
在 OSX 中
$ gcc -dynamic -fno-common -std=gnu99 -c -o module.o module.c
$ ld -o module.so module.o -bundle -undefined dynamic_lookup -lc
8. 加载我们的模块
构建模块后,你需要加载它。假设你已下载 Redis v4 或更高版本,你可以使用loadmodule 命令行参数运行它
$ redis-server --loadmodule /path/to/module.so
就是这样!Redis 现在正在运行,并且已加载我们的模块。我们可以简单地通过redis-cli 进行连接并运行我们的命令!
此处详述的完整源代码可在 RedisModuleSDK 的 example 目录中找到,其中还包含了一个模块项目模板、makefile 和一个实用程序库,该库包含一些功能(未包含在原始 API),它们可自动完成编写模块时一些比较枯燥的工作。你无需使用它,但可以根据喜好进行使用。