乐趣区

Redis-Module原理一

Redis 自 4.0 版本之后便支持了模块扩展功能, 在使用的过程中发现了一些 Redis 实现的疑问,

Redis 推荐开发人员通过引入 redismodule.h, 来调用指定接口来支持扩展, 其中要求实现程序必须实现 RedisModule_OnLoad 方法, 该方法主要加载模块, 注册相应的 api, 对 context 上下文注入,

那么它是什么时候被调用的呢? 又比如 Redis 为开发人员提供几组 API, 比如 RedisModule_StringPtrlen 用于返回抽象类型字符串的长度, 但是翻遍所有的代码也没有该函数声明的具体实现, 那么它是什么时候在哪里被实现的呢?
带着这些问题这篇文章会做出解答

(一) 使用

首先根据 官方文档 , 用户扩展模块需要通过配置文件指定需要加载的链接库

                    * 通过配置 loadmodule 指定用户扩展的链接库 *
                    

当然动态库需要用户自己去编译生成, 在编译之前需要将指定的 module 引入 redismodule.h 头文件, 最简单的方式是直接将源文件拉入 modules 下, 修改 MakeFile 文件并执行 make 编译

               * 修改 MakeFile*
       

那么 redis 是何时开始加载的呢?

(二) 链接库的加载

redis 服务端的入口在 server.c 源文件中, 其主要任务为初始化数据结构, 初始化设置, 启动 eventloop 事件加载器等等, 其中在启动事件加载器之前, redis 会先加载指定的链接库 (redis 也支持通过命令行加载库)

    server.sentinel_mode = checkForSentinelMode(argc,argv);
    initServerConfig();
    ACLInit(); /* The ACL subsystem must be initialized ASAP because the
                  basic networking code and client creation depends on it. */
    // 初始化模块 加载动态链接库
    moduleInitModulesSystem(); 

moduleInitModulesSystem 函数负责加载模块

loadmodule_queue 为 redis 需要加载模块路径数组, 会再 redis 初始化配置文件时添加

moduleRegisterCoreAPI 为注册函数的主要逻辑, 首先创建用于存放 api 函数的字典, 该字典 key 提供给 module 的函数名称, value 为 module.c 中具体的实现, 通过维护字典的方式实现了提供给模块的接口与 redis 内部实现相分离.

REGISTER_API() 为宏, 会将模块接口与具体实现函数做映射

到此为止已经为用户自定义的模块提供相应的 api 函数了, 这也解释了为什么 moduleapi 函数只有在 module.h 中但是找不到同函数名的实现, 原因是 redis 通过字典来维护了这层函数关系

那么 module 中创建的命令行函数又是如何注册到 redis 内部中呢?

(三) 命令行注册

用户自定义的命令行函数需要在 RedisModule_OnLoad 函数中通过以下方式注册

 if (RedisModule_CreateCommand(ctx,"api_RedisModule_Milliseconds",redisModule_Milliseconds, "readonly",0,0,0) == REDISMODULE_ERR){return REDISMODULE_ERR;}

函数的声明 (其中 redis 使用了大量的 incomplete type 后面会做出解释)

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

在 RedisModule_Init 函数中获取 RedisModule_CreateCommand 命令函数

static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) {void *getapifuncptr = ((void**)ctx)[0];
    // 设置 GetApi 函数 具体实现为 RM_GetApi 由 moduleLoad 函数初始化
    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);
    ...
}

实际上就是从初始创建的函数字典中获取相应的具体实现函数, 具体函数在 module.c 下的 RM_CreateCommand


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(cmdname) != NULL) {sdsfree(cmdname);
        return REDISMODULE_ERR;
    }
    ...
    // 注册命令
    dictAdd(server.commands,sdsdup(cmdname),cp->rediscmd);
    dictAdd(server.orig_commands,sdsdup(cmdname),cp->rediscmd);
    cp->rediscmd->id = ACLGetCommandID(cmdname); /* ID used for ACL. */
    return REDISMODULE_OK;
}

本次知识对 redis module 的加载提供大致的说明, 下期来详细讲讲实现细节

退出移动版