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
