vlambda博客
学习文章列表

Redis 模块机制 - 模块编写篇

在 Redis 应用中,模块机制是提及得比较少的一个功能,主要是 Redis 的功能基本上能应付各种需求,很少需要自己编写模块来扩展功能的。但有时候我们希望为 Redis 提供一些较为高级的功能,比如提供支持回滚的事务功能,这时就需要编写模块来进行扩展。

Redis 为模块编写者提供了丰富的 API 来操纵 Redis,下面我们编写一个简单的 Redis 模块来阐明模块编写的过程。

编写一个简单的 Redis 模块

我们要编写的模块只提供一个简单的功能,就是计算一个数的平方数,命令如下

$ 127.0.0.1:6379> math.double 10
(integer) 100

我们先把模块的代码贴出来,然后再解释代码的意义:

#include "redismodule.h"
#include <stdlib.h>

int MathDouble_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
long long num;

if (argc == 2) {
RedisModule_StringToLongLong(argv[1], &num);
RedisModule_ReplyWithLongLong(ctx, num * num);
return REDISMODULE_OK;
}

return RedisModule_ReplyWithError(ctx, "ERR invalid num parameters");
}

int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
if (RedisModule_Init(ctx, "math", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
return REDISMODULE_ERR;

if (RedisModule_CreateCommand(ctx, "math.double",
MathDouble_RedisCommand, "readonly", 1, 1, 1) == REDISMODULE_ERR)
return REDISMODULE_ERR;

return REDISMODULE_OK;
}

上面引入的 redismodule.h 文件是从 Redis 源码包中复制过来的,编写 Redis 模块时必须引入这个文件。

然后我们可以通过以下命令来编译这个模块:

$ gcc math.c -fPIC -shared -o math.so

上面的命令是把模块编译成 .so 文件,也就是共享库文件。然后可以通过 Redis 的 module load 命令来把模块加载到 Redis 中,如下:

$ module load ./math.so
OK

然后可以通过 module list 命令查看 Redis 已经加载的模块列表:

$ module list
1) 1) "name"
2) "math"
3) "ver"
4) (integer) 1

可以看到,我们的 math 模块已经被加载到 Redis 中。

接下来,我们就可以通过 math 模块提供的 math.double 命令来计算一个数的平方数,如下:

$ 127.0.0.1:6379> math.double 10
(integer) 100

如果看到上面的结果,那么恭喜你,因为你编写的模块已经被加入到 Redis 中。

模块代码解释

每个 Redis 模块都需要提供一个 RedisModule_OnLoad() 函数,这个函数是 Redis 加载模块时会调用的函数,也就是说,Redis 加载一个模块时,会调用这个模块的 RedisModule_OnLoad() 函数。 RedisModule_OnLoad() 函数的定义如下:

int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);

函数的第一个参数类型必须是 RedisModuleCtx,表示模块的上下文,主要用来存放模块的一些基本信息。第二个参数类型为 RedisModuleString,表示载入模块时传入的参数列表。第三个参数类型为 int,表示参数列表的个数。

在 RedisModule_OnLoad() 函数中一般需要调用 RedisModule_Init() 函数来向 Redis 注册模块,RedisModule_Init() 函数原型如下:

int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver);

第一个参数的类型前面已经介绍过,第二个参数用于指定模块的名称,第三个参数用于指定模块的版本,而第四个参数用于指定API的版本。

注册完模块后,就可以通过 RedisModule_CreateCommand() 函数来为模块创建命令。RedisModule_CreateCommand() 函数原型如下:

int RedisModule_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc,
const char *strflags, int firstkey, int lastkey, int keystep);

RedisModule_CreateCommand() 函数稍微有点复杂,下面介绍各个参数的作用:

  • ctx:就是模块的上下文。

  • name:用于指定命令的名称,比如上面我们定义的 math.double

  • cmdfunc:用于指定命令的功能函数,也就是当我们调用一个命令时,就会通过调用这个函数来实现具体的功能。

  • strflags:用于指定命令的功能,比如 write 表示命令对 Redis 进行写操作,而 readonly 表示命令只会读取 Redis 的数据等,为什么需要指定命令的功能呢?因为如果命令会对 Redis 进行写操作,那么 Redis 就需要把命令持久化。

  • firstkeylastkeykeystep:用于指导 command getkeys 命令(command getkeys 命令的使用可以参考 Redis 使用手册)获取 keys 列表时使用的,如果不提供这个功能,可以设置为0。

一个模块最重要的是命令的功能函数,如上面模块中的 MathDouble_RedisCommand() 函数,我们把 MathDouble_RedisCommand() 再展示一遍:

int MathDouble_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
long long num;

if (argc == 2) {
RedisModule_StringToLongLong(argv[1], &num);
RedisModule_ReplyWithLongLong(ctx, num * num);
return REDISMODULE_OK;
}

return RedisModule_ReplyWithError(ctx, "ERR invalid num parameters");
}

一个命令的功能函数的原型如下:

int xxx_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);

函数的名称可以随便定义,不过按照规范,一般以 _RedisCommand 结尾。第一个参数就是模块上下文,而第二个参数是命令的参数列表,第三个参数是参数列表的个数。

在上面的例子中,MathDouble_RedisCommand() 函数首先判断参数个数是否正确,如果不正确就返回 invalid num parameters 的错误信息。然而,Redis 提供了很多 API 让我们使用,比如 RedisModule_StringToLongLong() 函数就是把参数转换成 long long 类型的数值。而 RedisModule_ReplyWithLongLong() 函数可以让 Redis 回复一个整型数值给客户端。

Redis 提供的 API 非常多,具体可以参考 Redis 官网的模块开发介绍或者查看 redismodule.h 文件。Redis 模块编写的介绍就到这里了,下篇会介绍 Redis 模块内部的实现原理。

参考文献:

  1. Redis模块开发手册:Redis Modules: an introduction to the API

  2. Redis模块API手册 Modules API reference