暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

天凉好个秋 好个秋 2019-10-20
612

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


文章转载自天凉好个秋 好个秋,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论