圆点 极速的未来即将在你的城市里举办活动。

加入我们的 Redis 发布活动

编写 Redis 模块

Redis Enterprise modules around a database

我已经使用 Redis 大约六年了,多年来,我利用其少数几个数据类型做了一些非常多功能的事情,例如地理查询、文本搜索、机器学习等。

但是,多年来,我无法计算我已经有多少次(以及许多其他用户)希望 Redis 增加一些它所缺少的数据结构、命令或功能。因为没有它们,用户不得不诉诸不太优雅或不太高效的方法来实现我们在 Redis 中的目标。虽然 Lua 朝着正确的方向迈进了一步,但它也有自己的局限性。

但这些都正在发生改变,因为 Redis 现在拥有一个(尚未稳定) 的模块系统,它允许开发人员编写 C 库,以向 Redis 中添加新的功能和数据结构 - 其速度与普通的 Redis 命令相似。

我个人认为这是在很长一段时间里影响 Redis 最重要的一个新功能,它将超越 API - 在未来几年里显著影响 Redis 的动态和使用方式。

在过去几个月里,萨尔瓦托雷一直在编写一个相当大的模块 API,而我们 Redis 一直在忙于对此进行测试,创建一些模块来添加诸如认证、概率性数据结构和全文搜索等功能 - 所有这些都使用了新的数据结构,并在 Redis 中添加了新命令。

因此,我想分享一下我学到的东西,作为编写模块的一个基本指南。API 文档非常好,但也非常长。因此,请将本指南作为模块编写的 TL;DR 指南。

1. 并非你祖父时代的 Lua 脚本

让我们从基础知识开始:模块基本上是 C 共享库(.so 文件),Redis 可以在运行时或启动时加载它们。它们可以注册 Redis 不支持的新命令,并且可以访问 Redis 的数据来执行其操作。但有些关键的区别让模块变得更加强大

  • 首先,编译型代码的执行速度比解释型快几个数量级,所以除非你要做的是一个小的附加逻辑的宏,否则你应该看到模块的运行速度与 Redis 自身一样快。
  • 虽然 Lua 脚本只能在现有数据结构之上添加功能,但模块可以使用任何可想象的数据结构来创建新的数据类型。
  • Lua 脚本只能使用EVALEVALSHA进行调用,如果没有适当的客户端库,会让它们的调用有些繁琐。模块在 Redis 中添加的命令可以直接调用。FOO BAR BAZ - 就是这样!
  • 与 Lua 不同,模块可以绑定到第三方库中。例如,考虑 Unicode 处理、图片处理甚至是 GPU 上的代码执行。
  • 暴露给 Redis 模块的 API 远比暴露给 Lua 的最小化 API 更丰富。

2. 什么是一个模块

模块基本上包含的都是命令处理程序。这些是具有以下签名的 C 函数

int MyCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)

此理念非常简单,从标识可见:该函数返回一个整数,可能是 OK 或 ERR。通常,即使向用户返回错误,返回 OK 也是可以的。

命令处理程序接受一个 RedisModuleCtx* 对象。此对象对于模块开发人员来说是不透明的,但其内部包含调用客户机状态,甚至我们稍后会用到的内部内存管理。

接下来,它接收 argvargc,它们基本上是用户已传递给被调用命令的参数。第一个参数是调用的名称本身,其余的只是从 Redis 协议中解析而来的参数。

请注意,它们以 RedisModuleString 对象接收,它们同样是不透明的。如果需要操作,它们可以以零拷贝转换为常规 C 字符串。

要激活模块的命令,模块的标准入口点是一个名为 RedisModule_OnLoad 的函数。此函数告知 Redis 模块中有哪些命令并将它们映射到各自的处理程序。

让我们编写一个模块

好的,让我们编写一个 Redis 模块。我们将重点关注实现一个新的 Redis 命令的非常简单的模块示例 - HGETSET <key> <element> <new value>。它基本上是 HGETHSET 的组合,获取散列对象中的当前值,并将新值原子地设置为它的新值。这相当基础,可以使用一个简单的事务或 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 进行连接并运行我们的命令!

获取源代码

此处详述的完整源代码可在 RedisModuleSDKexample 目录中找到,其中还包含了一个模块项目模板、makefile 和一个实用程序库,该库包含一些功能(未包含在原始 API),它们可自动完成编写模块时一些比较枯燥的工作。你无需使用它,但可以根据喜好进行使用。