Redis4.0模块子系统实现简述

1、模块加载方法git

一、在配置文件或者启动参数里面经过<loadmodule /path/to/mymodule.so args>指令加载github

二、Redis启动后,经过<module load /path/to/mymodule.so args>指令加载,另外<module list>能够查询当前全部已加载模块。<module unload name>能够卸载已经加载的模块,注意name为模块的注册名字,不必定和模块文件名相同。redis


2、介绍
api

Redis模块是一种动态库,能够用与Redis内核类似的运行速度和特性来扩展Redis内核的功能。做者认为lua脚本只是组合Redis内核的现有功能,可是Redis模块则能够扩展Redis内核的功能。主要提供如下几个方面的扩展服务器

一、能够如lua脚本或者client同样,经过RedisModule_Call接口直接执行redis命令并获取执行结果。Redis称呼这种API为高层API。数据结构

二、能够经过RedisModule_OpenKey接口,获取底层键,并根据键的类型以及各种型提供的模块操做接口进行底层操做。函数

三、自动内存管理(Automatic memory management),能够在回调函数中,调用RedisModule_AutoMemory打开自动内存管理功能,这样随后分配的RedisModuleString对象、open key等,redis会记录下来,当回调函数返回的时候,redis会把这些资源自动释放调。这意味着不能在自动内存管理打开的状况下,建立RedisModuleString等对象来初始化全局变量。学习

四、redis本地类型(native types support)建立。经过提供RDB保存、RDB加载、AOF重写等回调函数,在Redis模块中能够建立相似redis内部dict、list之类的数据类型。例如能够在模块中建立一个链表,并提供对应的回调函数,这样redis在保存RDB文件的时候,就能够把模块中的数据保存在RDB中,在redis启动从rdb中加载数据的时候,进而能够恢复模块数据状态。测试

五、阻塞命令。在redis模块中能够将client阻塞,并设置超时时间。以实现相似BLPOP的阻塞命令。this


3、一个redis模块示例

以下代码一个简单的redis模块示例,添加了一个hello.rand命令。在模块加载的时候,打印出传入的参数,当执行hello.rand命令的时候,一样会打印出传入的命令参数,并返回生成的一个随机数。关于下面的代码,有两个点须要说明

一、RedisModule_OnLoad是每一个Redis模块的入口函数,在加载模块的时候,就是经过查找这个函数的入口地址来开始执行redis模块代码的。

二、RedisModule_Init是在调用redis模块API以前必须调用的初始化函数。通常应放在RedisModule_OnLoad的最开始位置。若是没有执行RedisModule_Init,就调用redis模块的API,则会产生空指针异常。

后面介绍redis实现的时候会进一步介绍上面的两点

 
 
 
 
 
#include "../../src/redismodule.h"#include <stdlib.h>#include <string.h>void HelloRedis_LogArgs(RedisModuleString **argv, int argc){    for (int j = 0; j < argc; j++) {        const char *s = RedisModule_StringPtrLen(argv[j],NULL);        printf("ARGV[%d] = %s\n", j, s);    }}int HelloRedis_RandCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {        HelloRedis_LogArgs(argv,argc);    RedisModule_ReplyWithLongLong(ctx,rand());    return REDISMODULE_OK;}int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {        if (RedisModule_Init(ctx,"hello",1,REDISMODULE_APIVER_1)        == REDISMODULE_ERR) return REDISMODULE_ERR;    HelloRedis_LogArgs(argv,argc);        if (RedisModule_CreateCommand(ctx,"hello.rand",        HelloRedis_RandCommand,"readonly",0,0,0)== REDISMODULE_ERR)        return REDISMODULE_ERR;                return REDISMODULE_OK;}

上面的模块编译执行后,client侧执行以下命令来进行测试。

 
 
 
 
 
127.0.0.1:6379> module load modules/hellomodule/helloRedis.so helloarg1 helloarg2 OK127.0.0.1:6379> module list1) 1) "name"   2) "hello"   3) "ver"   4) (integer) 1127.0.0.1:6379> hello.rand(integer) 1315916238127.0.0.1:6379> hello.rand(integer) 1420937835127.0.0.1:6379> hello.rand arg test(integer) 543546598127.0.0.1:6379> module unload helloOK

redis server端显示的以下内容。

 
 
 
 
 
ARGV[0] = helloarg1ARGV[1] = helloarg27779:M 19 Dec 14:33:17.032 * Module 'hello' loaded from modules/hellomodule/helloRedis.soARGV[0] = hello.randARGV[0] = hello.randARGV[0] = hello.randARGV[1] = argARGV[2] = test7779:M 19 Dec 14:34:13.604 * Module hello unloaded

4、redis模块管理相关数据结构

Redis模块管理涉及到的相关数据结构以下

 
 
 
 
 
struct RedisModule {    void *handle;   /* dlopen() 返回的handle. */    char *name;     /* 模块名字 */    int ver;        /* 模块版本*/    int apiver;     /* 模块API版本*/    list *types;    /* 用来保存模块的数据类型信息 */};typedef struct RedisModule RedisModule;static dict *modules; /* 全局变量  用来进行module_name(SDS) -> RedisModule ptr的hash查找*/struct moduleLoadQueueEntry {    sds path;    int argc;    robj **argv;};struct redisServer {    ....    list *loadmodule_queue;     //在redis启动的时候,用来保存命令行或者配置文件中的模块相关配置,每一个节点是一个struct moduleLoadQueueEntry    dict *moduleapi;            /* 导出的模块API名字与API地址的映射 后面介绍*/    ....};struct redisServer server; static list *moduleUnblockedClients;    //当模块中阻塞的client被RedisModule_UnblockClient接口解除阻塞的时候,会放入这个链表,后面统一处理

其中有几个须要额外说明一下

一、RedisModule中的types成员用来保存Redis模块中定义的native types,每一个数据类型对应一个节点。每一个节点的类型为struct RedisModuleType,里面包含了rdb_load、rdb_save、aof_rewrite等回调函数,这里没有给出struct RedisModuleType。

二、server.loadmodule_queue这个队列里面保存了redis经过命令行或者配置文件传入的模块加载信息,每一个节点类型为struct moduleLoadQueueEntry。如配置文件指定"module load /path/to/mymodule.so arg1 arg2",则会构建一个struct moduleLoadQueueEntry,其中path成员为包含/path/to/mymodule.so的SDS,argc=2,argv则包含两个robj对象指针,robj对象分别包含着"arg1"和"arg2"。

为何没有在加载配置的时候,直接加载模块,而是先保存到队列中呢?缘由是在加载配置的时候,redis server尚未完成初始化,加载模块的时候,会调用模块中的RedisModule_OnLoad函数,若是此时模块访问Redis内部数据,那么可能会访问到无效的数据。所以须要加载的模块须要先保存在队列中,等redis初始化完毕后,在从队列中依次加载对应的模块。

三、关于moduleUnblockedClients,当模块调用RedisModule_UnblockClient的时候,会先把要解除阻塞的client加入到这个链表中,等待当前redis的文件事件和时间事件处理完毕后,等待下一次事件前(beforeSleep->moduleHandleBlockedClients),来集中处理(例如调用模块注册的reply_callback函数等)。

这里为何没有直接在RedisModule_UnblockClient中处理,而是先添加到一个链表中,后面由redis内核处理呢?缘由是RedisModule_UnblockClient在模块中支持线程调用,而redis内核事件处理是单线程的,所以为了不线程竞争会先把待解除阻塞的client放入到moduleUnblockedClients链表中,后续交由redis内核处理。


5、module命令实现

接着说一下module命令中load、unload、list等实现

首先经过配置文件、命令行或者module load命令加载模块的时候,以下执行

 
 
 
 
 
/* 加载一个模块并初始化. 成功返回 C_OK , 失败返回C_ERR */int moduleLoad(const char *path, void **module_argv, int module_argc) {    int (*onload)(void *, void **, int);    void *handle;    RedisModuleCtx ctx = REDISMODULE_CTX_INIT;        //加载动态库    handle = dlopen(path,RTLD_NOW|RTLD_LOCAL);    if (handle == NULL) {        return C_ERR;    }    //查找动态库中入口函数RedisModule_OnLoad的地址    onload = (int (*)(void *, void **, int))(unsigned long) dlsym(handle,"RedisModule_OnLoad");    if (onload == NULL) {        return C_ERR;    }        //执行模块中的RedisModule_OnLoad入口函数    if (onload((void*)&ctx,module_argv,module_argc) == REDISMODULE_ERR) {        if (ctx.module) moduleFreeModuleStructure(ctx.module);        dlclose(handle);        return C_ERR;    }    /* Redis module 加载成功,注册到modules全局字典中 */    dictAdd(modules,ctx.module->name,ctx.module);    ctx.module->handle = handle;    /*注意这里会把ctx释放掉,后面须要的时候,会根据modules字典中的查找到的模块信息,构造一个ctx     *这意味着在模块函数中的ctx入参是一个堆栈上的变量,     *例如经过RedisModule_AutoMemory设置ctx自动内存管理的时候,只是当次有效*/    moduleFreeContext(&ctx);    return C_OK;}

module unload命令卸载一个模块时候,执行以下简化代码

 
 
 
 
 
/* 卸载一个模块,成功返回C_OK,失败返回C_ERR */int moduleUnload(sds name) {    struct RedisModule *module = dictFetchValue(modules,name);    if (module == NULL) {        return REDISMODULE_ERR;    }    //若是模块导入了本地数据类型,则不容许卸载    if (listLength(module->types)) {        return REDISMODULE_ERR;    }    /* 模块能够向Redis服务器注册新的Redis命令,卸载模块的时候,须要取消以前注册的命令 */    unregister_cmds_of_module(module);    /* 卸载动态库 */    if (dlclose(module->handle) == -1) {        char *error = dlerror();        if (error == NULL) error = "Unknown error";    }    /* 从全局modules字典中删除模块 同时释放module->name*/    dictDelete(modules,module->name);    module->name = NULL;    //释放module占用的内存    moduleFreeModuleStructure(module);    return REDISMODULE_OK;}

module list命令执行以下简化代码

 
 
 
 
 
/* modules list简化代码 */void moduleList(sds name) {    dictIterator *di = dictGetIterator(modules);    dictEntry *de;    addReplyMultiBulkLen(c,dictSize(modules));    //遍历modules字典,获取每一个模块的名字和版本    while ((de = dictNext(di)) != NULL) {        sds name = dictGetKey(de);        struct RedisModule *module = dictGetVal(de);        addReplyMultiBulkLen(c,4);        addReplyBulkCString(c,"name");        addReplyBulkCBuffer(c,name,sdslen(name));        addReplyBulkCString(c,"ver");        addReplyLongLong(c,module->ver);    }    dictReleaseIterator(di);}

6、模块导出符号与Redis core函数映射

在Redis提供给模块的API中,API的名字都是相似RedisModule_<funcname>的形式,实际对应Redis core中的RM_<funcname>函数。目前只有一个例外就是RedisModule_Init这个模块API在Redis core中的名字也是RedisModule_Init。上面咱们讲过,RedisModule_Init应该是模块入口RedisModule_OnLoad中第一个调用的函数。而RedisModule_OnLoad的工做就是完成了RedisModule_<funcname>与RM_<funcname>之间的关联创建关系。

下面咱们首先以上面示例模块中的RedisModule_CreateCommand这个模块API为例,说明怎么关联到RM_CreateCommand上的,而后在说明为何这样设计。

一、RedisModule_<funcname>与RM_<funcname>关联创建过程

1.一、首先在Redis启动的时候,会执行下面的初始化代码

 
 
 
 
 
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)/* Register all the APIs we export. Keep this function at the end of the * file so that's easy to seek it to add new entries. */void moduleRegisterCoreAPI(void) {    server.moduleapi = dictCreate(&moduleAPIDictType,NULL);    ...    //其余的接口一样须要经过REGISTER_API来注册    REGISTER_API(CreateCommand);    REGISTER_API(SetModuleAttribs);    ...}

上面代码等效于

 
 
 
 
 
//在server.moduleapi中将字符串"RedisModule_<funcname>"与函数RM_<funcname>的地址创建关联dictAdd(server.moduleapi, "RedisModule_CreateCommand", RM_CreateCommand)dictAdd(server.moduleapi, "RedisModule_SetModuleAttribs", RM_SetModuleAttribs)

1.二、在模块源码中包含redismodule.h头文件的时候,会把下面的代码包含进来

 
 
 
 
 
#define REDISMODULE_API_FUNC(x) (*x)//其余的模块接口一样须要经过REDISMODULE_API_FUNC来定义与RM_<funcname>一致的函数指针RedisModule_<funcname>int REDISMODULE_API_FUNC(RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep);int REDISMODULE_API_FUNC(RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver);#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_GetApi = (int (*)(const char *, void *)) (unsigned long)getapifuncptr;    ...    //其余模块接口一样须要经过REDISMODULE_GET_API来初始化RedisModule_<funcname>指针    REDISMODULE_GET_API(CreateCommand);    REDISMODULE_GET_API(SetModuleAttribs);    ...    RedisModule_SetModuleAttribs(ctx,name,ver,apiver);    return REDISMODULE_OK;}

上面代码进行宏展开后等效以下

 
 
 
 
 
//定义与RM_<funcname>类型一致的函数指针RedisModule_<funcname>int (*RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep);int (*RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver);static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) {    void *getapifuncptr = ((void**)ctx)[0];    RedisModule_GetApi = (int (*)(const char *, void *)) (unsigned long)getapifuncptr;    ...    //其余模块接口一样须要经过REDISMODULE_GET_API来初始化RedisModule_<funcname>指针    RedisModule_GetApi("RedisModule_CreateCommand",((void **)&RedisModule_CreateCommand);    RedisModule_GetApi("RedisModule_SetModuleAttribs",((void **)&RedisModule_SetModuleAttribs);    ...    RedisModule_SetModuleAttribs(ctx,name,ver,apiver);    return REDISMODULE_OK;}

1.三、在上面moduleLoad加载模块的时候,咱们看到会传递RedisModuleCtx ctx = REDISMODULE_CTX_INIT做为入参,调用RedisModule_OnLoad,并在RedisModule_OnLoad中调用RedisModule_Init。

 
 
 
 
 
#define REDISMODULE_CTX_INIT {(void*)(unsigned long)&RM_GetApi, NULL, NULL, NULL, 0, 0, 0, NULL, 0, NULL, NULL, 0, NULL}/* 查找模块请求的API,并保存在targetPtrPtr中 */int RM_GetApi(const char *funcname, void **targetPtrPtr) {    dictEntry *he = dictFind(server.moduleapi, funcname);    if (!he) return REDISMODULE_ERR;    *targetPtrPtr = dictGetVal(he);    return REDISMODULE_OK;}

所以在函数RedisModule_Init实际执行的时候,至关于把RedisModule_<funcname>指针初始化为RM_<funcname>函数的地址了。所以随后在模块中调用RedisModule_<funcname>的时候,实际上调用的是RM_<funcname>。

二、为何采用这种设计?

实际上在redismodule.h头文件或者模块源码中直接extern RM_<funcname>,也是能够直接访问RM_<funcname>这个函数的。那么为何要在每一个模块的源码中定一个指向RM_<funcname>的函数指针RedisModule_<funcname>,并经过RedisModule_<funcname>来访问模块API呢?


主要是考虑到后续升级的灵活性,模块能够有不一样的API版本,虽然目前API版本只有一个,可是假如后续升级后,Redis支持了新版本的API。那么当不一样API版本的模块向Redis注册的时候,Redis内核就能够根据注册的API版本,来把不一样模块中的函数指针指向不一样的API实现函数了。这相似以面向对象中依赖于抽象而不是依赖具体的设计思路。


补充说明:

一、在redis源码src/modules目录下给出了一些redis模块相关的示例和说明文档,是不错的学习资料。

二、https://github.com/antirez/redis/commit/85919f80ed675dad7f2bee25018fec2833b8bbde




相关文章
相关标签/搜索