优秀的编程知识分享平台

网站首页 > 技术文章 正文

Redis 模块学习(上):What And How

nanyue 2024-08-01 22:47:22 技术文章 7 ℃

What

Redis Module系统是Redis4引入的一个新特性(很惭愧的是现在才知道,一直以为是Redis5引进的新特性)。通过Redis Module可以扩展Redis本身的能力,能够实现一些Redis本身不支持的命令。

虽然Redis已经提供了Lua脚本的编程能力来扩展Redis的能力,但是Lua脚本只能组合现有的数据结构和命令,没法创建新的命令。除此之外,Redis模块还具有以下几个优点:

  1. 作为一个C语言开发的库,Redis Module消耗更少的资源,运行的更快。而且在开发Redis模块的过程中还可以调用第三方的库。
  2. Redis模块实现的命令可以直接被客户端调用,就好像Redis原生的命令一样。而Lua脚本只能通过EVAL/EVALSHA命令进行调用。
  3. 暴露给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;
}

第一个参数是模块上下文,第二个参数是命令的入参,第三个参数表示参数的个数。

整个方法的逻辑很简单,首先是校验参数,然后打开源数据列表和目标,对列表中的每个元素进行判断是否符合要求,如果符合的话则把对应的数据放进目标列表中,并统计符合要求的数据的个数作为最终的返回结果。

但是有几点需要注意的是:

  1. 内存管理。 在模块开发中,开发者可以自己进行内存管理(调用RedisModule_Alloc()、RedisModule_Realloc()或者RedisModule_Calloc()分配内存,调用RedisModule_Free()释放内存)。自己管理内存很容易造成内存泄露等问题,因此Redis提供了自动内存管理机制,只需要调用RedisModule_AutoMemory()方法即可。但是有一点很重要的就是如果模块实现需要让Redis自动内存管理,则需要在模块实现的最开始的地方就要调用RedisModule_AutoMemory()方法
  2. RedisModule_OpenKey()。 RedisModule_OpenKey()方法会返回代表对应的Redis Key的句柄,如果对应的key不存在,且传入的是WRITTE,则依然会返回句柄,在之后对key进行写操作时会生成对应的键值对。如果传入的是READ则直接返回NULL,但是这样不会影响后续的RedisModule_CloseKey()方法。

Why

Redis源码学习之模块(下)

Reference

  1. Redis 5.X under the hood: 3 — Writing a Redis Module
  2. Redis Modules: an introduction to the API
  3. Modules API reference

Tags:

最近发表
标签列表