leevis.com
leevis.com copied to clipboard
redis module 开发与解析
redis module 开发与解析
概述
redis作者antirez前几天在自己的博客更新了一篇文章说redis4.0支持module。他自己写了一个神经网络的模块,也有开发者写了图表DB、二级索引、全文索引等模块。并且说这只是刚刚开始,你可以写一些有意义的不止是和缓存相关的模块。
相关的代码已经在github上开源,在github.com/antirez仓库redis4.0分支。
redis是通过开放的一系列API来支持模块开发,并且在代码中给出了几个实例,在redis4.0分支的github.com/antirez/redis/src/modules路径下。
模块开发示例
通过一个代码示例来说明如何开发一个模块。下面是官方helloworld.c的代码的一部分。
cat hello.c
// 必须包含
#include "../redismodule.h"
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
// 返回当前选中的db
int HelloSimple_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
REDISMODULE_NOT_USED(argv);
REDISMODULE_NOT_USED(argc);
RedisModule_ReplyWithLongLong(ctx,RedisModule_GetSelectedDb(ctx));
return REDISMODULE_OK;
}
// 实现类似rpush的功能
int HelloPushNative_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
{
if (argc != 3) return RedisModule_WrongArity(ctx);
RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1],
REDISMODULE_READ|REDISMODULE_WRITE);
RedisModule_ListPush(key,REDISMODULE_LIST_TAIL,argv[2]);
size_t newlen = RedisModule_ValueLength(key);
RedisModule_CloseKey(key);
RedisModule_ReplyWithLongLong(ctx,newlen);
return REDISMODULE_OK;
}
// 调用原始的rpush命令
int HelloPushCall_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
{
if (argc != 3) return RedisModule_WrongArity(ctx);
RedisModuleCallReply *reply;
reply = RedisModule_Call(ctx,"RPUSH","ss",argv[1],argv[2]);
long long len = RedisModule_CallReplyInteger(reply);
RedisModule_FreeCallReply(reply);
RedisModule_ReplyWithLongLong(ctx,len);
return REDISMODULE_OK;
}
// 模块入口,每个模块必须实现该函数。
// 调用RedisModule_Init注册模块,调用RedisModule_CreateCommand注册命令
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
// 初始化hello模块,hello是模块名称和文件名没关系,想定义什么定义什么。但最好和模块同名。
if (RedisModule_Init(ctx,"hello",1,REDISMODULE_APIVER_1)
== REDISMODULE_ERR) return REDISMODULE_ERR;
// 打印配置文件loadmodule path args... 的args部分。也就是说模块也是支持传递参数的。
for (int j = 0; j < argc; j++) {
const char *s = RedisModule_StringPtrLen(argv[j],NULL);
printf("Module loaded with ARGV[%d] = %s\n", j, s);
}
// 注册hello.simple命令对应的处理逻辑
if (RedisModule_CreateCommand(ctx,"hello.simple",
HelloSimple_RedisCommand,"readonly",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
// 注册hello.push.native命令对应的处理逻辑
if (RedisModule_CreateCommand(ctx,"hello.push.native",
HelloPushNative_RedisCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
// 注册hello.push.call命令对应的处理逻辑
if (RedisModule_CreateCommand(ctx,"hello.push.call",
HelloPushCall_RedisCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR)
return REDISMODULE_ERR;
return REDISMODULE_OK;
}
编译: macosx 系统:
cc -I. -W -Wall -dynamic -fno-common -g -ggdb -std=c99 -O2 -fPIC -c hello.c -o hello.xo
ld -o hello.so hello.xo -bundle -undefined dynamic_lookup -lc
linux 系统:
cc -I. -W -Wall -fno-common -g -ggdb -std=c99 -O2 -fPIC -c hello.c -o hello.xo
ld -o hello.so hello.xo -shared -lc
配置文件: cat ./test-redis.conf
loadmodule ./modules/hello.so hello liwq
bind 127.0.0.1
port 8899
protected-mode yes
supervised no
databases 2
save 10 1
启动:
./redis-server ./test-redis.conf
......
Module loaded with ARGV[0] = hello
Module loaded with ARGV[1] = liwq
......
测试:
~/Work redis-cli -h 127.0.0.1 -p 8899
127.0.0.1:8899> keys *
(empty list or set)
127.0.0.1:8899> select 0
OK
127.0.0.1:8899> hello.simple
(integer) 0
127.0.0.1:8899> hello.push.native book medis
(integer) 1
127.0.0.1:8899> type book
list
127.0.0.1:8899> lrange book 0 -1
1) "medis"
127.0.0.1:8899> hello.push.call book nginx
(integer) 2
127.0.0.1:8899> lrange book 0 -1
1) "medis"
2) "nginx"
127.0.0.1:8899>
源码解析
通过分析redis源码来说明redis是如何支持模块开发的。
redis启动时先调用moduleInitModulesSystem初始化模块环境并注册api,然后再调用loadServerConfig加载配置文件并调用queueLoadModule函数把loadmodule配置的模块添加到server.loadmodule_queue队列。解析完配置文件后,调用moduleLoadFromQueue函数遍历该队列,调用moduleLoad函数执行模块的初始化函数。
初始化模块环境并调用moduleRegisterCoreAPI注册模块api:
void moduleInitModulesSystem(void) {
moduleUnblockedClients = listCreate();
server.loadmodule_queue = listCreate();
modules = dictCreate(&modulesDictType,NULL);
moduleRegisterCoreAPI();
}
注册模块api,调用宏REGISTER_API注册api:
void moduleRegisterCoreAPI(void) {
server.moduleapi = dictCreate(&moduleAPIDictType,NULL);
REGISTER_API(Alloc);
REGISTER_API(Calloc);
REGISTER_API(Realloc);
REGISTER_API(Free);
REGISTER_API(Strdup);
REGISTER_API(CreateCommand);
......
}
宏REGISTER_API定义,实际上就是把函数的地址和api名称(名称统一添加RedisModule_前缀)以kv的形式添加到server.moduleapi字典中:
int moduleRegisterApi(const char *funcname, void *funcptr) {
return dictAdd(server.moduleapi, (char*)funcname, funcptr);
}
#define REGISTER_API(name) \
moduleRegisterApi("RedisModule_" #name, (void *)(unsigned long)RM_ ## name)
加载配置文件的模块,并初始化:
/* Load a module and initialize it. On success C_OK is returned, otherwise
* C_ERR is returned. */
int moduleLoad(const char *path, void **module_argv, int module_argc) {
int (*onload)(void *, void **, int);
void *handle;
// module 上下文
RedisModuleCtx ctx = REDISMODULE_CTX_INIT;
handle = dlopen(path,RTLD_NOW|RTLD_LOCAL);
if (handle == NULL) {
serverLog(LL_WARNING, "Module %s failed to load: %s", path, dlerror());
return C_ERR;
}
// onload 指向模块中RedisModule_OnLoad函数,因此每个模块都必须实现该函数
onload = (int (*)(void *, void **, int))(unsigned long) dlsym(handle,"RedisModule_OnLoad");
if (onload == NULL) {
serverLog(LL_WARNING,
"Module %s does not export RedisModule_OnLoad() "
"symbol. Module not loaded.",path);
return C_ERR;
}
// 执行RedisModule_OnLoad函数
if (onload((void*)&ctx,module_argv,module_argc) == REDISMODULE_ERR) {
if (ctx.module) moduleFreeModuleStructure(ctx.module);
dlclose(handle);
serverLog(LL_WARNING,
"Module %s initialization failed. Module not loaded",path);
return C_ERR;
}
/* Redis module loaded! Register it. */
// 初始化好的模块上下文添加到modules字典中
dictAdd(modules,ctx.module->name,ctx.module);
ctx.module->handle = handle;
serverLog(LL_NOTICE,"Module '%s' loaded from %s",ctx.module->name,path);
moduleFreeContext(&ctx);
return C_OK;
}
看下我们hello模块RedisModule_OnLoad函数的实现,先调用RedisModule_Init函数注册模块,再调用RedisModule_CreateCommand注册命令:
if (RedisModule_Init(ctx,"hello",1,REDISMODULE_APIVER_1)
== REDISMODULE_ERR) return REDISMODULE_ERR;
......
if (RedisModule_CreateCommand(ctx,"hello.simple",
HelloSimple_RedisCommand,"readonly",0,0,0) == REDISMODULE_ERR)
return REDISMODULE_ERR;
......
调用宏REDISMODULE_GET_API注册模块api函数:
// 宏REDISMODULE_GET_API定义
#define REDISMODULE_GET_API(name) \
RedisModule_GetApi("RedisModule_" #name, ((void **)&RedisModule_ ## name))
......
static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) {
void *getapifuncptr = ((void**)ctx)[0];
// 宏REDISMODULE_GET_API 实际上是封装了RedisModule_GetApi函数,实际上是调用的RM_GetApi函数
// RM_GetApi 函数是从server.moduleapi这个字典中取到函数的地址。
RedisModule_GetApi = (int (*)(const char *, void *)) (unsigned long)getapifuncptr;
REDISMODULE_GET_API(Alloc);
REDISMODULE_GET_API(Calloc);
REDISMODULE_GET_API(Free);
REDISMODULE_GET_API(Realloc);
REDISMODULE_GET_API(Strdup);
REDISMODULE_GET_API(CreateCommand);
REDISMODULE_GET_API(SetModuleAttribs);
.....
RedisModule_SetModuleAttribs(ctx,name,ver,apiver);
return REDISMODULE_OK;
}
所以RedisModule_SetModuleAttribs函数实际上还是module.c中函数RM_SetModuleAttribs。
void RM_SetModuleAttribs(RedisModuleCtx *ctx, const char *name, int ver, int apiver){
RedisModule *module;
if (ctx->module != NULL) return;
module = zmalloc(sizeof(*module));
module->name = sdsnew((char*)name);
module->ver = ver;
module->apiver = apiver;
module->types = listCreate();
ctx->module = module;
}
RedisModule_CreateCommand函数实际上还是module.c中的RM_CreateCommand. 把命令以及对应处理函数添加到server.commands字典中。
int RM_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep) {
int flags = strflags ? commandFlagsFromString((char*)strflags) : 0;
if (flags == -1) return REDISMODULE_ERR;
if ((flags & CMD_MODULE_NO_CLUSTER) && server.cluster_enabled)
return REDISMODULE_ERR;
struct redisCommand *rediscmd;
RedisModuleCommandProxy *cp;
sds cmdname = sdsnew(name);
/* Check if the command name is busy. */
if (lookupCommand((char*)name) != NULL) {
sdsfree(cmdname);
return REDISMODULE_ERR;
}
/* Create a command "proxy", which is a structure that is referenced
* in the command table, so that the generic command that works as
* binding between modules and Redis, can know what function to call
* and what the module is.
*
* Note that we use the Redis command table 'getkeys_proc' in order to
* pass a reference to the command proxy structure. */
cp = zmalloc(sizeof(*cp));
cp->module = ctx->module;
cp->func = cmdfunc;
cp->rediscmd = zmalloc(sizeof(*rediscmd));
cp->rediscmd->name = cmdname;
cp->rediscmd->proc = RedisModuleCommandDispatcher;
cp->rediscmd->arity = -1;
cp->rediscmd->flags = flags | CMD_MODULE;
cp->rediscmd->getkeys_proc = (redisGetKeysProc*)(unsigned long)cp;
cp->rediscmd->firstkey = firstkey;
cp->rediscmd->lastkey = lastkey;
cp->rediscmd->keystep = keystep;
cp->rediscmd->microseconds = 0;
cp->rediscmd->calls = 0;
dictAdd(server.commands,sdsdup(cmdname),cp->rediscmd);
dictAdd(server.orig_commands,sdsdup(cmdname),cp->rediscmd);
return REDISMODULE_OK;
}
总结:
通过官方提供的api,我们可以利用redis高性能的框架实现我们自己的特殊的功能,如果官方提供的api不能满足我们的要求,我们也可以修改module.c文件添加自己的函数,在moduleRegisterCoreAPI中注册。并修改redismodule.h文件,定义一个api的函数指针,如:void *REDISMODULE_API_FUNC(RedisModule_Test)(void args);
。 并在RedisModule_Init中调用REDISMODULE_GET_API为函数指针赋值,这样就可以在模块开发中调用RedisModule_Test函数了。
師兄會否知曉RedisModule_CreateCommand
最後的三個arguments: firstkey, lastkey, keystep
有何作用?
另外發現了一個坑,於CLI環境下調用,如果argument用quote包著,i.e. "Peter Nelson"
,後面一定要帶一個space,否則會出現Invalid argument(s)
,師兄有沒有遇到過?