What
Redis Module系统是Redis4引入的一个新特性(很惭愧的是现在才知道,一直以为是Redis5引进的新特性)。通过Redis Module可以扩展Redis本身的能力,能够实现一些Redis本身不支持的命令。
虽然Redis已经提供了Lua脚本的编程能力来扩展Redis的能力,但是Lua脚本只能组合现有的数据结构和命令,没法创建新的命令。除此之外,Redis模块还具有以下几个优点:
- 作为一个C语言开发的库,Redis Module消耗更少的资源,运行的更快。而且在开发Redis模块的过程中还可以调用第三方的库。
- Redis模块实现的命令可以直接被客户端调用,就好像Redis原生的命令一样。而Lua脚本只能通过EVAL/EVALSHA命令进行调用。
- 暴露给Redis模块的API比Lua脚本的要丰富的多。
How
加载模块
Redis模块可以通过在Redis的配置文件redis.conf中添加下面指令来加载:
loadmodule /path/to/module.so
也可以在Redis已经启动之后的运行时通过module命令来加载模块:
MODULE LOAD /path/to/module.so 127.0.0.1:6379> module list (empty list or set) 127.0.0.1:6379> module load /Users/Slogen/Documents/code/slogen/module/module.o OK 127.0.0.1:6379> module list 1) 1) "name" 2) "list_extend" 3) "ver" 4) (integer) 1 127.0.0.1:6379>
可以使用MODULE LIST命令来列出已经加载的模块。
使用MODULE UNLOAD命令来卸载已经加载的模块:
127.0.0.1:6379> module unload list_extend OK 127.0.0.1:6379> module list (empty list or set)
Redis模块API在设计支出就考虑到了将来(可能出现的)兼容性问题,所以无论Redis核心发生了什么变化,你现在开发的Redis模块,只要API不变,4年后仍然能运行。基于这些基本原理,设计了两种不同的API来访问Redis的数据空间。
- 第一种是一些低级API。这些API提供了一系列操作可以更快速的访问以及操作Redis的数据结构。Redis开发者可以创建同Redis原生命令一样的强大且高性能的命令。
- 另一种API高级API可以调用Redis命令并且获取命令执行结果,就如同Lua脚本访问Redis一样。
开发自己的模块
开发一个模块最直接的方式就是从Redis的官方仓库下载redismodule.h文件,然后拷贝到自己的项目里面去。实现自己的模块的逻辑,链接所有依赖的第三方库,最后生成一个导出了RedisModule_OnLoad()方法的动态链接库。
我们这次要用C语言开发一个扩展模块:LIST_EXTEND,这个模块有个命令FILTER。 调用方式如下:
LIST_EXTEND.FILTER SOURCE_LIST DEST_LIST LOW_VALUE HIGH_VALUE
这个命令会去读取指定(list类型的)SOURCE_LIST的数据,然后按照下面的规则进行过滤:
LOW_VALUE <= ELEMENT VALUE <= HIGH_VALUE
然后新生成一个新的列表(如果存在则先删除),过滤之后的数据设置为为这个新的列表的数据。
如果LOW_VALUE > MAX_VALUE,结果就是一个空列表。
其中LOW_VALUE和MAX_VALUE可以是字符串-inf和+inf表示不限制最小值和最大值。
这个命令会返回新列表元素的个数。
127.0.0.1:6379> module load /Users/slogen/Documents/code/slogen/module/module.o OK 127.0.0.1:6379> LPUSH source_list 20 2 30 1 40 (integer) 5 127.0.0.1:6379> LIST_EXTEND.FILTER source_list destination_list 2 20 (integer) 2 127.0.0.1:6379> LRANGE destination_list 0 -1 1) "2" 2) "20" 127.0.0.1:6379>
首先下载redismodule.h文件放到自己的项目目录,然后新建一个.c文件:mymodule.c(文件名随意):
// // Created by Slogen on 2019/10/16. // #include "redismodule.h" #include <limits.h> // 检查给定的参数是否超过限制 int checkLimit(RedisModuleString *argv,RedisModuleString *expect,long long *value,int dValue) { if(0 == RedisModule_StringCompare(argv,expect)) { *value = dValue; return 1; } else { if(REDISMODULE_OK == RedisModule_StringToLongLong(argv,value)) { return 1; } } return 0; } int ListExtendFilter_RedisCommand(RedisModuleCtx *ctx,RedisModuleString **argv,int argc) { if(argc != 5) { return RedisModule_WrongArity(ctx); } // 让redis自动管理内存 RedisModule_AutoMemory(ctx); // 打开源数据key RedisModuleKey *sourceListkey = (RedisModuleKey *)RedisModule_OpenKey(ctx,argv[1],REDISMODULE_WRITE | REDISMODULE_READ); int sourceListKeyType = RedisModule_KeyType(sourceListkey); if(REDISMODULE_KEYTYPE_LIST != sourceListKeyType && REDISMODULE_KEYTYPE_EMPTY != sourceListKeyType) { RedisModule_CloseKey(sourceListkey); return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE); } // 打开目标key RedisModuleKey *destinationListKey = (RedisModuleKey *)RedisModule_OpenKey(ctx,argv[2],REDISMODULE_WRITE); RedisModule_DeleteKey(destinationListKey); // 获取源数据列表的长度 size_t sourceListLentth = RedisModule_ValueLength(sourceListkey); if(0 == sourceListLentth) { RedisModule_ReplyWithLongLong(ctx,0L); return REDISMODULE_OK; } char strLowerLimit[] = "-inf"; char strUpperLimit[] = "+inf"; RedisModuleString *expectedMinusInf,*expectedPlusInf; long long lowerLimit; long long upperLimit; int lowerLimitOk = 0; int upperLimitOk = 0; expectedMinusInf = RedisModule_CreateString(ctx,strLowerLimit,4); expectedPlusInf = RedisModule_CreateString(ctx,strUpperLimit,4); lowerLimitOk = checkLimit(argv[3],expectedMinusInf,&lowerLimit,LONG_MIN); upperLimitOk = checkLimit(argv[4],expectedPlusInf,&upperLimit,LONG_MAX); // the number of added elements to the destination_list size_t added = 0; for(size_t pos = 0;pos < sourceListLentth;++pos) { RedisModuleString *ele = RedisModule_ListPop(sourceListkey,REDISMODULE_LIST_TAIL); RedisModule_ListPush(sourceListkey,REDISMODULE_LIST_HEAD,ele); long long val; if(REDISMODULE_OK == RedisModule_StringToLongLong(ele,&val) && 1 == lowerLimitOk && 1 == upperLimitOk) { if(val >= lowerLimit && val <= upperLimit) { RedisModuleString *newele = RedisModule_CreateStringFromLongLong(ctx,val); // push to destination_list if(REDISMODULE_ERR == RedisModule_ListPush(destinationListKey,REDISMODULE_LIST_HEAD,newele)) { return REDISMODULE_ERR; } added++; } } } RedisModule_ReplyWithLongLong(ctx,added); RedisModule_ReplicateVerbatim(ctx); return REDISMODULE_OK; } // Redis 模块的入口函数 int RedisModule_OnLoad(RedisModuleCtx *ctx,RedisModuleString **argv,int argc) { if(REDISMODULE_ERR == RedisModule_Init(ctx,"list_extend",1,REDISMODULE_APIVER_1)) { return REDISMODULE_ERR; } if(REDISMODULE_ERR == RedisModule_CreateCommand(ctx,"list_extend.filter",ListExtendFilter_RedisCommand,"write deny-oom",1,1,1)) { return REDISMODULE_ERR; } return REDISMODULE_OK; }
模块开发完毕之后,需要编译:
gcc -fPIC -shared -std=gnu99 -o module.o module.c
上述命令会在当前目录生成module.o文件,然后连接Redis Server,使用MODULE LOAD命令加载模块。
127.0.0.1:6379> module load /Users/slogen/Documents/code/slogen/module/module.o OK 127.0.0.1:6379> LPUSH source_list 20 2 30 1 40 (integer) 5 127.0.0.1:6379> LIST_EXTEND.FILTER source_list destination_list 2 20 (integer) 2 127.0.0.1:6379> LRANGE destination_list 0 -1 1) "2" 2) "20"
如果一切顺利的话,模块已经被加载到Redis系统了,但是有可能会出现报错:
Module module.o does not export RedisModule_OnLoad() symbol. Module not loaded
而且检查了代码,的确存在RedisModule_OnLoad()方法,那么一个可能的原因是代码是用C++写的而不是C(扩展名是.cpp而不是.c)。
C++编译器在对源码进行编译的时候会做名字转换,所以源码中的RedisModule_OnLoad方法名会被转换成其他的名字。
? module nm module.o | grep OnLoad 0000000000001520 T __Z18RedisModule_OnLoadP14RedisModuleCtxPP17RedisModuleStringi
模块原理分析
Redis模块的入口点在于RedisModule_OnLoad()方法,在这个方法里面,可以对模块进行初始化,注册模块命令以及模块中可能会用到的数据结构。
现在来看下RedisModule_OnLoad()方法的实现:
int RedisModule_OnLoad(RedisModuleCtx *ctx,RedisModuleString **argv,int argc) { // 1. 初始化模块属性 if(REDISMODULE_ERR == RedisModule_Init(ctx,"list_extend",1,REDISMODULE_APIVER_1)) { return REDISMODULE_ERR; } // 2. 注册模块命令。 // "write": 模块命令可能会修改数据,也会读取数据 // "deny-oom": 命令使用过程中需要使用额外的内存,因此需要预防出现oom的情况 if(REDISMODULE_ERR == RedisModule_CreateCommand(ctx,"list_extend.filter",ListExtendFilter_RedisCommand,"write deny-oom",1,1,1)) { return REDISMODULE_ERR; } return REDISMODULE_OK; }
在RedisModule_OnLoad()方法中首先要调用RedisModule_Init()方法方法。
RedisModule_Init()方法的原型如下:
static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver);
RedisModule_Init()方法会向Redis模块系统注册该模块的名称、版本以及自己需要的API的版本。
在RedisModule_Init()方法中会完成Redis模块系统对外暴露的API的注册,这个后面会讲。
接下来需要调用RedisModule_CreateCommand()方法来注册模块的命令,该方法原型如下:
int RedisModule_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep);
其中几个关键的参数含义为:
- name: 命令的名称。
- cmdfunc: 命令的实现,函数指针。
- strflags: 这个参数用来标识模块命令的具体行为,需要是一个空格分隔的C风格的字符串。本次开发的模块传入的是"write deny-oom"(其他的一些属性后面再说)。
- write: 表示模块会修改数据(也会读取数据)。
- deny-oom: 表示命令需要额外的内存,为了预防出现OOM的情况,在内存不够的情况下拒绝执行。
如果没有任何报错的话就返回REDISMODULE_OK常量。
在调用RedisModule_CreateCommand()方法的时候传入了命令回调句柄,现在来看看命令的实现ListExtendFilter_RedisCommand()方法:
int ListExtendFilter_RedisCommand(RedisModuleCtx *ctx,RedisModuleString **argv,int argc) { // 1. 检查参数个数是都符合要求 if(argc != 5) { return RedisModule_WrongArity(ctx); } // 2. 让redis自动管理内存:注意:如果需要模块系统自动管理内存,则需要首先调用这个方法 RedisModule_AutoMemory(ctx); // 3. 打开源数据key RedisModuleKey *sourceListkey = (RedisModuleKey *)RedisModule_OpenKey(ctx,argv[1],REDISMODULE_WRITE | REDISMODULE_READ); // 4. 获取源数据key类型 int sourceListKeyType = RedisModule_KeyType(sourceListkey); // 5. 校验类型 if(REDISMODULE_KEYTYPE_LIST != sourceListKeyType && REDISMODULE_KEYTYPE_EMPTY != sourceListKeyType) { RedisModule_CloseKey(sourceListkey); return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE); } // 6. 打开结果key RedisModuleKey *destinationListKey = (RedisModuleKey *)RedisModule_OpenKey(ctx,argv[2],REDISMODULE_WRITE); // 如果结果key是可写的前存在则先删除对应的数据 RedisModule_DeleteKey(destinationListKey); // 7. 获取源数据长度 size_t sourceListLentth = RedisModule_ValueLength(sourceListkey); if(0 == sourceListLentth) { RedisModule_ReplyWithLongLong(ctx,0L); return REDISMODULE_OK; } char strLowerLimit[] = "-inf"; char strUpperLimit[] = "+inf"; RedisModuleString *expectedMinusInf,*expectedPlusInf; long long lowerLimit; long long upperLimit; int lowerLimitOk = 0; int upperLimitOk = 0; expectedMinusInf = RedisModule_CreateString(ctx,strLowerLimit,4); expectedPlusInf = RedisModule_CreateString(ctx,strUpperLimit,4); // 8. 获取上下限 lowerLimitOk = checkLimit(argv[3],expectedMinusInf,&lowerLimit); upperLimitOk = checkLimit(argv[4],expectedPlusInf,&upperLimit); // the number of added elements to the destination_list size_t added = 0; for(size_t pos = 0;pos < sourceListLentth;++pos) { RedisModuleString *ele = RedisModule_ListPop(sourceListkey,REDISMODULE_LIST_TAIL); RedisModule_ListPush(sourceListkey,REDISMODULE_LIST_HEAD,ele); long long val; if(REDISMODULE_OK == RedisModule_StringToLongLong(ele,&val) && 1 == lowerLimitOk && 1 == upperLimitOk) { if(val >= lowerLimit && val <= upperLimit) { // 9. 如果数据满足要求,则push进目标列表中 RedisModuleString *newele = RedisModule_CreateStringFromLongLong(ctx,val); // push to destination_list if(REDISMODULE_ERR == RedisModule_ListPush(destinationListKey,REDISMODULE_LIST_HEAD,newele)) { return REDISMODULE_ERR; } added++; } } } RedisModule_ReplyWithLongLong(ctx,added); RedisModule_ReplicateVerbatim(ctx); return REDISMODULE_OK; }
第一个参数是模块上下文,第二个参数是命令的入参,第三个参数表示参数的个数。
整个方法的逻辑很简单,首先是校验参数,然后打开源数据列表和目标,对列表中的每个元素进行判断是否符合要求,如果符合的话则把对应的数据放进目标列表中,并统计符合要求的数据的个数作为最终的返回结果。
但是有几点需要注意的是:
- 内存管理。 在模块开发中,开发者可以自己进行内存管理(调用RedisModule_Alloc()、RedisModule_Realloc()或者RedisModule_Calloc()分配内存,调用RedisModule_Free()释放内存)。自己管理内存很容易造成内存泄露等问题,因此Redis提供了自动内存管理机制,只需要调用RedisModule_AutoMemory()方法即可。但是有一点很重要的就是如果模块实现需要让Redis自动内存管理,则需要在模块实现的最开始的地方就要调用RedisModule_AutoMemory()方法。
- RedisModule_OpenKey()。 RedisModule_OpenKey()方法会返回代表对应的Redis Key的句柄,如果对应的key不存在,且传入的是WRITTE,则依然会返回句柄,在之后对key进行写操作时会生成对应的键值对。如果传入的是READ则直接返回NULL,但是这样不会影响后续的RedisModule_CloseKey()方法。
Why
Redis源码学习之模块(下)
Reference
- Redis 5.X under the hood: 3 — Writing a Redis Module
- Redis Modules: an introduction to the API
- Modules API reference