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

一、模块加载方法

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

2、Redis启动后,通过<module load /path/to/mymodule.so args>指令加载,另外<module list>可以查询当前所有已加载模块。<module unload name>可以卸载已经加载的模块,注意name为模块的注册名字,不一定和模块文件名相同。

二、介绍

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

1、可以如lua脚本或者client一样,通过RedisModule_Call接口直接执行redis命令并获取执行结果。Redis称呼这种API为高层API。

2、可以通过RedisModule_OpenKey接口,获取底层键,并根据键的类型以及各类型提供的模块操作接口进行底层操作。

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

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

5、阻塞命令。在redis模块中可以将client阻塞,并设置超时时间。以实现类似BLPOP的阻塞命令。

三、一个redis模块示例

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

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

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

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

  1. #include "../../src/redismodule.h"
  2. #include <stdlib.h>
  3. #include <string.h>
  4. void HelloRedis_LogArgs(RedisModuleString **argv, int argc)
  5. {
  6.    for (int j = 0; j < argc; j++) {
  7.        const char *s = RedisModule_StringPtrLen(argv[j],NULL);
  8.        printf("ARGV[%d] = %s\n", j, s);
  9.    }
  10. }
  11. int HelloRedis_RandCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
  12.    
  13.    HelloRedis_LogArgs(argv,argc);
  14.    RedisModule_ReplyWithLongLong(ctx,rand());
  15.    return REDISMODULE_OK;
  16. }
  17. int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
  18.    
  19.    if (RedisModule_Init(ctx,"hello",1,REDISMODULE_APIVER_1)
  20.        == REDISMODULE_ERR) return REDISMODULE_ERR;
  21.    HelloRedis_LogArgs(argv,argc);
  22.    
  23.    if (RedisModule_CreateCommand(ctx,"hello.rand",
  24.        HelloRedis_RandCommand,"readonly",0,0,0)== REDISMODULE_ERR)
  25.        return REDISMODULE_ERR;
  26.        
  27.    
  28.    return REDISMODULE_OK;
  29. }

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

  1. 127.0.0.1:6379> module load modules/hellomodule/helloRedis.so helloarg1 helloarg2
  2. OK
  3. 127.0.0.1:6379> module list
  4. 1) 1) "name"
  5.   2) "hello"
  6.   3) "ver"
  7.   4) (integer) 1
  8. 127.0.0.1:6379> hello.rand
  9. (integer) 1315916238
  10. 127.0.0.1:6379> hello.rand
  11. (integer) 1420937835
  12. 127.0.0.1:6379> hello.rand arg test
  13. (integer) 543546598
  14. 127.0.0.1:6379> module unload hello
  15. OK

redis server端显示的如下内容。

  1. ARGV[0] = helloarg1
  2. ARGV[1] = helloarg2
  3. 7779:M 19 Dec 14:33:17.032 * Module ‘hello‘ loaded from modules/hellomodule/helloRedis.so
  4. ARGV[0] = hello.rand
  5. ARGV[0] = hello.rand
  6. ARGV[0] = hello.rand
  7. ARGV[1] = arg
  8. ARGV[2] = test
  9. 7779:M 19 Dec 14:34:13.604 * Module hello unloaded

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

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

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

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

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

2、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初始化完毕后,在从队列中依次加载对应的模块。

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

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

五、module命令实现

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

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

  1. /* 加载一个模块并初始化. 成功返回 C_OK , 失败返回C_ERR */
  2. int moduleLoad(const char *path, void **module_argv, int module_argc) {
  3.    int (*onload)(void *, void **, int);
  4.    void *handle;
  5.    RedisModuleCtx ctx = REDISMODULE_CTX_INIT;
  6.    
  7.    //加载动态库
  8.    handle = dlopen(path,RTLD_NOW|RTLD_LOCAL);
  9.    if (handle == NULL) {
  10.        return C_ERR;
  11.    }
  12.    //查找动态库中入口函数RedisModule_OnLoad的地址
  13.    onload = (int (*)(void *, void **, int))(unsigned long) dlsym(handle,"RedisModule_OnLoad");
  14.    if (onload == NULL) {
  15.        return C_ERR;
  16.    }
  17.    
  18.    //执行模块中的RedisModule_OnLoad入口函数
  19.    if (onload((void*)&ctx,module_argv,module_argc) == REDISMODULE_ERR) {
  20.        if (ctx.module) moduleFreeModuleStructure(ctx.module);
  21.        dlclose(handle);
  22.        return C_ERR;
  23.    }
  24.    /* Redis module 加载成功,注册到modules全局字典中 */
  25.    dictAdd(modules,ctx.module->name,ctx.module);
  26.    ctx.module->handle = handle;
  27.    /*注意这里会把ctx释放掉,后面需要的时候,会根据modules字典中的查找到的模块信息,构造一个ctx
  28.     *这意味着在模块函数中的ctx入参是一个堆栈上的变量,
  29.     *例如通过RedisModule_AutoMemory设置ctx自动内存管理的时候,只是当次有效*/
  30.    moduleFreeContext(&ctx);
  31.    return C_OK;
  32. }

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

  1. /* 卸载一个模块,成功返回C_OK,失败返回C_ERR */
  2. int moduleUnload(sds name) {
  3.    struct RedisModule *module = dictFetchValue(modules,name);
  4.    if (module == NULL) {
  5.        return REDISMODULE_ERR;
  6.    }
  7.    //如果模块导入了本地数据类型,则不允许卸载
  8.    if (listLength(module->types)) {
  9.        return REDISMODULE_ERR;
  10.    }
  11.    /* 模块可以向Redis服务器注册新的Redis命令,卸载模块的时候,需要取消之前注册的命令 */
  12.    unregister_cmds_of_module(module);
  13.    /* 卸载动态库 */
  14.    if (dlclose(module->handle) == -1) {
  15.        char *error = dlerror();
  16.        if (error == NULL) error = "Unknown error";
  17.    }
  18.    /* 从全局modules字典中删除模块 同时释放module->name*/
  19.    dictDelete(modules,module->name);
  20.    module->name = NULL;
  21.    //释放module占用的内存
  22.    moduleFreeModuleStructure(module);
  23.    return REDISMODULE_OK;
  24. }

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

  1. /* modules list简化代码 */
  2. void moduleList(sds name) {
  3.    dictIterator *di = dictGetIterator(modules);
  4.    dictEntry *de;
  5.    addReplyMultiBulkLen(c,dictSize(modules));
  6.    //遍历modules字典,获取每个模块的名字和版本
  7.    while ((de = dictNext(di)) != NULL) {
  8.        sds name = dictGetKey(de);
  9.        struct RedisModule *module = dictGetVal(de);
  10.        addReplyMultiBulkLen(c,4);
  11.        addReplyBulkCString(c,"name");
  12.        addReplyBulkCBuffer(c,name,sdslen(name));
  13.        addReplyBulkCString(c,"ver");
  14.        addReplyLongLong(c,module->ver);
  15.    }
  16.    dictReleaseIterator(di);
  17. }

六、模块导出符号与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上的,然后在说明为什么这样设计。

1、RedisModule_<funcname>与RM_<funcname>关联建立过程

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

  1. int moduleRegisterApi(const char *funcname, void *funcptr) {
  2.    return dictAdd(server.moduleapi, (char*)funcname, funcptr);
  3. }
  4. #define REGISTER_API(name) \
  5.    moduleRegisterApi("RedisModule_" #name, (void *)(unsigned long)RM_ ## name)
  6. /* Register all the APIs we export. Keep this function at the end of the
  7. * file so that‘s easy to seek it to add new entries. */
  8. void moduleRegisterCoreAPI(void) {
  9.    server.moduleapi = dictCreate(&moduleAPIDictType,NULL);
  10.    ...
  11.    //其他的接口同样需要通过REGISTER_API来注册
  12.    REGISTER_API(CreateCommand);
  13.    REGISTER_API(SetModuleAttribs);
  14.    ...
  15. }

上面代码等效于

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

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

  1. #define REDISMODULE_API_FUNC(x) (*x)
  2. //其他的模块接口同样需要通过REDISMODULE_API_FUNC来定义与RM_<funcname>一致的函数指针RedisModule_<funcname>
  3. int REDISMODULE_API_FUNC(RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep);
  4. int REDISMODULE_API_FUNC(RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver);
  5. #define REDISMODULE_GET_API(name) \
  6.    RedisModule_GetApi("RedisModule_" #name, ((void **)&RedisModule_ ## name))
  7. static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) {
  8.    void *getapifuncptr = ((void**)ctx)[0];
  9.    RedisModule_GetApi = (int (*)(const char *, void *)) (unsigned long)getapifuncptr;
  10.    ...
  11.    //其他模块接口同样需要通过REDISMODULE_GET_API来初始化RedisModule_<funcname>指针
  12.    REDISMODULE_GET_API(CreateCommand);
  13.    REDISMODULE_GET_API(SetModuleAttribs);
  14.    ...
  15.    RedisModule_SetModuleAttribs(ctx,name,ver,apiver);
  16.    return REDISMODULE_OK;
  17. }

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

  1. //定义与RM_<funcname>类型一致的函数指针RedisModule_<funcname>
  2. int (*RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep);
  3. int (*RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver);
  4. static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) {
  5.    void *getapifuncptr = ((void**)ctx)[0];
  6.    RedisModule_GetApi = (int (*)(const char *, void *)) (unsigned long)getapifuncptr;
  7.    ...
  8.    //其他模块接口同样需要通过REDISMODULE_GET_API来初始化RedisModule_<funcname>指针
  9.    RedisModule_GetApi("RedisModule_CreateCommand",((void **)&RedisModule_CreateCommand);
  10.    RedisModule_GetApi("RedisModule_SetModuleAttribs",((void **)&RedisModule_SetModuleAttribs);
  11.    ...
  12.    RedisModule_SetModuleAttribs(ctx,name,ver,apiver);
  13.    return REDISMODULE_OK;
  14. }

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

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

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

2、为什么采用这种设计?

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

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

补充说明:

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

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

来自为知笔记(Wiz)

时间: 2024-08-06 16:04:52

Redis4.0模块子系统实现简述的相关文章

redis-4.0.14 cluster 配置实战

1.操作系统配置 切换到root用户修改配置sysctl.conf vim /etc/sysctl.conf # 添加配置: vm.max_map_count=655360 vm.overcommit_memory=1 net.core.somaxconn= 32767 fs.file-max=65535 # ulimit -n 最大文件描述符 65536 vim /etc/security/limits.conf # 添加 * soft nofile 65536 * hard nofile 6

CentOS6.9安装Redis4.0.0

防伪码:没有相当程度的孤独是不可能有内心的平和. 1.环境准备 主机名 IP 系统 redis-01 192.168.10.24 CentOS release 6.9 (Final) Redis安装比较简单,官网下载界面就有参考: https://redis.io/download 2.安装 # 创建安装目录 mkdir /app # 下载redis4.0.0 cd /opt wget http://download.redis.io/releases/redis-4.0.0.tar.gz # 

Redis4.0 主从复制(PSYN2.0)

Redis4.0版本相比原来3.x版本,增加了很多新特性,如模块化.PSYN2.0.非阻塞DEL和FLUSHALL/FLUSHDB.RDB-AOF混合持久化等功能.尤其是模块化功能,作者从七年前的redis1.0版本就开始谋划,终于在4.0版本发布了,所以版本号也就从3.x直接迭代到了4.0以表示版本变化之大.简单看了一下新版的PSYN2.0,虽然很多细节没搞清楚,但是大概流程倒是搞明白了. 一.主要流程 在新版的PSYN2.0中,相比原来的PSYN功能,最大的变化支持两种场景下的部分重同步,一

USGS-EROS项目espa-surface-reflectance中的LaSRC Version 1.3.0模块利用vs2010编译出windows64位版本(一)

Landsat8卫星的大气校正目前国内有很多学者都在做,随便百度一下就能找到很多论文,提出的算法都有各自的亮点,学术研究上都有意义. 但是,问题来了,如果要真正拿出来处理任意一幅Landsat8的图像,具备工程化数据处理能力,且平均结果精度被广泛认可的算法程序,选哪个? 目前,我认为首选USGS-EROS项目espa-surface-reflectance中的LaSRC Version 1.3.0模块.机构够强,项目都大,Eric Vermote参与算法.额,做大气校正的都知道这个人的吧. 代码

CentOS7安装Redis4.0

1. 安装CentOS7,关闭防火墙 systemctl stop firewalld.service #停止firewallsystemctl disable firewalld.service #禁止firewall开机启动 2 安装Redis需要用GCC编译.先安装GCC yum install gcc make 3. 通过wget下载redis wget http://download.redis.io/releases/redis-4.0.1.tar.gz  这里的下载地址可以去red

Linux上编译Redis4.0.2

Linux上安装部署Redis4.0.2 安装Redis4.0.2,需要先安装依赖: yum -y install gcc gcc-c++ libstdc++-devel tcl -y 下载Redis4.0.2的安装包: wget http://219.238.7.71/files/403400000ABE0C0C/download.redis.io/releases/redis-4.0.2.tar.gz 解压缩: tar -zxvf redis-4.0.2.tar.gz -C /usr/loc

redis-4.0.1安装配置(CentOS 6.6)

一.redis服务安装配置 1.        下载解压redis软件包 # wget http://download.redis.io/releases/redis-4.0.1.tar.gz # tar zxvf redis-4.0.1.tar.gz # cd redis-4.0.1 2.        编译安装redis # make MALLOC=jemalloc # make PREFIX=/application/redis-4.0.1 install # ln -s /applica

linux7.0下2台服务器安装redis4.0.2集群

分配端口和ip 172.16.1.23 redis01 172.16.1.24 redis02   redis01:7000 redis02:7003     redis01:7001   redis02:7004    redis01:7002 redis02:7005   1 安装gcc  Redis在linux上的安装首先必须先安装gcc,这个是用来编译redis的源文件的 # yum install gcc -y 2.下载解压redis的源文件  https://redis.io/dow

(十)Linux安装Redis-4.0.8

Redis的安装 1.上传 redis-4.0.8.tar.gz 到/usr/local/src文件夹 2.解压 tar xzf redis-4.0.8.tar.gz 3.cd redis-4.0.8 4.编译 make(因为下载的源码,需要编译) 编译完成之后,可以看到解压文件redis-4.0.8 中会有对应的src文件夹.conf文件. 5.编译成功后,进入src文件夹,执行make install进行Redis安装 Redis的部署 安装成功后,下面对Redis 进行部署, 1.首先为了