我使用 Redis 大约六年了,多年来我利用它有限的数据类型做了一些非常多样化的事情,比如地理查询、文本搜索、机器学习等等。
但这些年来,我都数不清自己(以及许多其他用户)有多少次希望 Redis 能拥有它所缺乏的这种或那种数据结构、命令或功能。没有它们迫使用户不得不采取不那么优雅或效率不高的方法来实现我们在 Redis 中的目标。虽然 Lua 是朝着正确方向迈出的一步,但它也有其局限性。
但这都在改变,因为 Redis 现在有了一个(尚未稳定)模块系统,允许开发者编写 C 库,为 Redis 添加新的功能和数据结构——速度堪比正常的 Redis 命令。
我个人认为这是 Redis 长期以来最令人兴奋的新功能,它将超越 API 的范畴——极大地塑造未来 Redis 的动态和使用方式。
在过去的几个月里,当 Salvatore 致力于庞大的模块 API 时,我们 Redis 团队一直在忙于对其进行实验,创建了添加身份验证、概率数据结构和全文搜索等功能的模块——所有这些都使用了新的数据结构并为 Redis 添加了新的命令。
所以我想分享我学到的一些经验,作为编写模块的基本指南。API 文档非常好,但也很长。因此,请将本指南视为编写模块的 TL;DR(太长不读)指南。
让我们从基础开始:模块本质上是 C 共享库(.so 文件),Redis 可以在运行时或启动时加载它们。它们可以注册 Redis 不支持的新命令,并可以访问 Redis 的数据来完成其任务。但是,模块与 Lua 脚本有一些关键区别,这使得模块强大得多
模块主要包含的是命令处理器。这些是具有以下签名的 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 wrong number of arguments for ‘get’ command
3. 激活 AutoMemory
Redis Modules API 的一个强大特性是自动资源和内存管理。虽然模块作者可以独立分配和释放内存,但创建 Redis 资源是由系统管理的,如果在处理器生命周期内分配了 Redis 字符串、键和响应,它们将自动释放,前提是我们调用了 RedisModule_AutoMemory。
RedisModule_AutoMemory(ctx);
4. 执行 Redis 调用
现在我们将执行两个 Redis 调用中的第一个:HGET。我们将 argv[1] 和 argv[2] 作为参数传递,它们分别是 key 和 element。我们使用通用的 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-string,“d” 表示 double,“l” 表示 long,“b” 表示 c-buffer(一个字符串及其长度)。
现在让我们执行第二个 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] 有一个 key
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 中。你不必使用它,但可以随意使用。