leevis.com icon indicating copy to clipboard operation
leevis.com copied to clipboard

redis module 开发与解析

Open vislee opened this issue 8 years ago • 1 comments

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函数了。

vislee avatar Dec 15 '16 07:12 vislee

師兄會否知曉RedisModule_CreateCommand最後的三個arguments: firstkey, lastkey, keystep有何作用? 另外發現了一個坑,於CLI環境下調用,如果argument用quote包著,i.e. "Peter Nelson",後面一定要帶一個space,否則會出現Invalid argument(s),師兄有沒有遇到過?

cscan avatar May 01 '20 01:05 cscan