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实现的时候会进一步介绍上面的两点
#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
OK
127.0.0.1:6379> module list
1) 1) "name"
2) "hello"
3) "ver"
4) (integer) 1
127.0.0.1:6379> hello.rand
(integer) 1315916238
127.0.0.1:6379> hello.rand
(integer) 1420937835
127.0.0.1:6379> hello.rand arg test
(integer) 543546598
127.0.0.1:6379> module unload hello
OK
redis server端显示的如下内容。
ARGV[0] = helloarg1
ARGV[1] = helloarg2
7779:M 19 Dec 14:33:17.032 * Module 'hello' loaded from modules/hellomodule/helloRedis.so
ARGV[0] = hello.rand
ARGV[0] = hello.rand
ARGV[0] = hello.rand
ARGV[1] = arg
ARGV[2] = test
7779:M 19 Dec 14:34:13.604 * Module hello unloaded
四、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接口解除阻塞的时候,会放入这个链表,后面统一处理
其中有几个需要额外说明一下
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命令加载模块的时候,如下执行
/* 加载一个模块并初始化. 成功返回 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);
}
六、模块导出符号与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启动的时候,会执行下面的初始化代码
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.2、在模块源码中包含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.3、在上面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>。
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
Redis4.0模块子系统实现简述的更多相关文章
- Redis(二)CentOS7安装Redis4.0.10与集群搭建
一 Redis单机安装 1 Redis下载安装 1.1 检查依赖环境(Redis是C语言开发,编译依赖gcc环境) [root@node21 redis-]$ gcc -v -bash: gcc: c ...
- Redis4.0 Cluster — Centos7
本文版权归博客园和作者吴双本人共同所有 转载和爬虫请注明原文地址 www.cnblogs.com/tdws 一.基础安装 wget http://download.redis.io/releases/ ...
- redis-4.0.8 配置文件解读
# Redis configuration file example.## Note that in order to read the configuration file, Redis must ...
- Redis-4.0.11集群配置
版本:redis-3.0.5 redis-3.2.0 redis-3.2.9 redis-4.0.11 参考:http://redis.io/topics/cluster-tutorial. 集群 ...
- Redis4.0新特性(一)-Memory Command
Redis4.0版本增加了很多诱人的新特性,在redis精细化运营管理中都非常有用(猜想和antirez加入redislabs有很大关系):此系列几篇水文主要介绍以下几个新特性的使用和效果. Redi ...
- redis-4.0.14 cluster 配置实战
1.操作系统配置 切换到root用户修改配置sysctl.conf vim /etc/sysctl.conf # 添加配置: vm.max_map_count= vm.overcommit_memor ...
- 【zigbee无线通信模块步步详解】ZigBee3.0模块建立远程网络控制方法
本文以路灯控制应用为例,简述ZigBee3.0模块使用流程. 一.建立网络 1.通过USB转串口模块将出厂的ZigBee自组网模块连接,打开上位机软件"E180-ZG120A-Setting ...
- Redis4.0.0 安装及配置 (Linux — Centos7)
本文中的两个配置文件可在这里找到 操作系统:Linux Linux发行版:Centos7 安装 下载地址,点这里Redis4.0.0.tar.gz 或者使用命令: wget http://downlo ...
- linux 安装redis4.0.6
1.进入/usr/local/src目录,下载redis # cd /usr/local/src# wget http://download.redis.io/releases/redis-4.0.6 ...
随机推荐
- Glide Golang包管理
Golang的包管理乱得不行,各种工具横空出世,各显神通啊.用了几个下来,发现 Glide 是比较好用的,使用了 vender 来进行管理,多个开发环境的版本不冲突,功能强大,配置文件也足够简单. 初 ...
- 经典算法--冒泡排序(Java)
原理:将相邻元素的较大值赋给右边 思路:① 1.将集合或数组内的第一个元素与第二个元素进行比较,较大值赋给右边: 2.将第二个元素与第三个元素进行比较,较大值赋给右边: ....... (N-1).将 ...
- Java基础—IO小结(一)概述与节点流
一.File类的使用 由于file类是一个基础类,所以我们从file类开始了解.(SE有完善的中文文档,建议阅读) 构造器: 常用方法:——完整方法请参见API API API!!! File做的是 ...
- JavaEE笔记(二)
查询load()和get()的区别 # 以下查询都是根据id查询 // Load和Get都会在第一次查询的是创建一个一级缓存查询语句 // 下一次查询的时候从缓存中查询是否有缓存的语句 // 如果有只 ...
- 学习笔记:Oracle的trace文件可见性
隐藏参数: _trace_files_public 参数 trace文件的默认权限: - r w - r - - - - - 如果设定 trace_files_public参数为 true, 则 t ...
- 前端- css - 总结
1.css层叠样式表 1.什么是CSS? CSS是指层叠样式表(Cascading Style Sheets),样式定义如何显示HTML元素,样式通常又会存在于样式表中. 也就是说把HTML元素的样式 ...
- idea tomcat热部署 Error running 'Tomcat 7': Unable to open debugger port (127.0.0.1:3622): java.net.SocketExcepti
2018/5/6 经过测试,发现只需要修改 http port 为 8081即可,JMX port 不用改 默认是 1099 今天在进 tomcat 的 debug 模式时报了此异常, tomcat ...
- Intellij IDEA《十分钟,配置struts2》by me
1.加载Struts 2类库 <dependencies> <!-- Struts 2 核心包--> <dependency> <groupId>org ...
- 从《乱世王者》看腾讯SLG手游如何搭建完整安全服务
WeTest 导读 <乱世王者>是由腾讯旗下天美工作室群自主研发的一款战争策略手游,在经历了2015年-2017年的SLG品类手游的爆发之势下,于2017年11月21日正式公测. < ...
- (2017)你最不建议使用的Python Web框架?
https://www.sohu.com/a/164042813_737973 挺有意思的 经过一周的Django学习,以及对比,最终选定了以Flask入手来学习Python web开发.