Redis——Module模块

x33g5p2x  于2022-03-24 转载在 Redis  
字(20.7k)|赞(0)|评价(0)|浏览(782)

redis4.0以上版本,用户可以在redis定义自己的扩展module了,可以快速实现新的命令和功能;

redis模块使用动态库,可以在redis启动的时候加载或者启动后使用命令加载,API定义在了一个叫redismodule.h的头文件里,可以使用c++或者其他c语言绑定的程序编写模块;

加载模块的两种方法:

  • 启动时使用redis.conf配置的模块:

配置文件里配置loadmodule /path/to/my_module.so

  • 客户端登录后使用命令加载和卸载:

MODULE LIST 展示当前加载的所有模块;

MODULE LOAD my_module.so 加载模块;(so文件)

MODULE UNLOAD modulename 卸载模块;(模块名字,MODULE LIST展示的那个名字,非文件名,通常文件名和模块名同名)

模块的简单介绍:(当前使用redis版本v6.2.6)

编写模块不需要依赖redis或者其他库,只需要#include "redismodule.h"这个文件就可以编写了(该头文件里定义了一些常量以及可以使用的API),该文件在下载解压好的src目录下,复制出来就可以在模块里用了,模块可以在不同版本的redis里使用,另外api如果在当前redis版本里没有实现会被赋值为NULL,可以使用NULL或者使用RMAPI_FUNC_SUPPORTED(func)判断该api可不可用,不过前提是存在该api:

api官网:Modules API reference – Redis

模块里必须有一个RedisModule_OnLoad()函数,是模块入口点,可以在函数里定义模块名、新命令、新数据类型等等,函数原型:

int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)

参数1是上下文(通常函数第一个参数都是ctx),参数2是传入参数,参数3是传入参数的数量;参数2可以通过配置文件里loadmodule /path/to/my_module.so arg1 arg2或者使用命令MODULE LOAD my_module.so arg1 arg2传入,所以可以根据传入的参数使用不同的新命令;

OnLoad函数里最先被调用的函数应该是Init函数,Init函数用来注册模块名(MODULE LIST展示的模块名,卸载时使用的名),函数原型:

int RedisModule_Init(RedisModuleCtx *ctx, const char *modulename, int module_version, int api_version);

参数1是上下文,参数2是模块名,参数3是模块版本号,参数4是api版本号(好像一直是REDISMODULE_APIVER_1变量);模块名不能跟已有的有冲突;

注册新命令使用CreateCommand函数,必须在OnLoad函数里使用,函数原型:

int RedisModule_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep);

参数1是上下文,参数2是命令名称,参数3是实现该命令的函数,参数4是命令标志(只读,写等等,多个标志中间用空格分开),参数5是第一个参数索引(索引是从1开始的,0的位置是命令本身,0表示命令无key),参数6是最后一个参数索引(负数表示从后开始,-1表示提供的最后一个参数,0表示命令无key),参数7是第一个参数和最后一个参数之间的距离(0表示命令无key);(上面的索引设定只用来查找索引处的key,对于复杂参数可以传递0,0,0,并使用RedisModule_SetCommandInfo设置更详细的参数)

实现命令的函数原型:

int mycommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);

参数1是上下文,参数2是用户传入的参数,参数3是用户传入参数的数量;(argv[1]才是用户传入的第一个参数,argv[0]是命令名,命令无参数的时候argc为1,所以argc通常>=1)

(通常参数数量错误的时候使用return RedisModule_WrongArity(ctx);直接返回错误,h头文件里定义了这个错误用于说明参数数量错误)

实现命令的函数里给用户返回数据可以使用以RedisModule_ReplyWith开头的api函数返回不同类型的数据,这些api定义在h头文件里:

模块可以被卸载,大多数情况不需要特殊处理,卸载模块时redis会自动注销命令和取消订阅消息,有时候命令里有一些持久内存或者配置,可以使用OnUnload函数进行处理,在模块卸载的时候将被自动调用,函数原型:

int RedisModule_OnUnload(RedisModuleCtx *ctx);

OnUnload函数正常应该返回REDISMODULE_OK,但是返回REDISMODULE_ERR可以阻止模块被卸载;

另外,有个一叫自动内存管理的函数,在自己函数的开始的地方调用该函数即可,因为通常c程序员需要自己管理内存,所以模块api有释放字符串、关闭开启的键、释放回复等功能,redis也提供了自动内存管理(会消耗一些性能),函数原型:

RedisModule_AutoMemory(ctx);

使用自动内存管理后,不需要关闭打开的键,不需要释放回复,不需要释放RedisModuleString对象;当然启用自动内存管理时,也支持手动释放,尤其分配了大量字符串需要尽快释放的时候可以使用手动加快释放;

编写一个小Demo:

test.c文件(将传入的数字乘以2后返回)

#include "redismodule.h"

int MyCmd(RedisModuleCtx *ctx, RedisModuleString ** argv, int argc)
{
        if (argc != 2)
        {
                return RedisModule_WrongArity(ctx);
        }
        RedisModule_AutoMemory(ctx);
        long long myparam;
        if (RedisModule_StringToLongLong(argv[1], &myparam) == REDISMODULE_OK)
        {
                RedisModule_ReplyWithLongLong(ctx, myparam * 2);
        }
        else
        {
                RedisModule_ReplyWithError(ctx, "ERROR wrong type of arguments");
        }
        return REDISMODULE_OK;
}
int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv,int argc)
{
        if (RedisModule_Init(ctx, "mymodule", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
        {
                return REDISMODULE_ERR;
        }
        if (RedisModule_CreateCommand(ctx, "mycmd", MyCmd, "readonly", 0, 0, 0) == REDISMODULE_ERR)
        {
                return REDISMODULE_ERR;
        }

        return REDISMODULE_OK;
}

编译成so:执行gcc test.c -fPIC -shared -o test.so

使用redis-cli登录redis,加载so文件并测试新命令:

关于RedisModuleString对象:

调用api传递参数、返回值,用到的基本都是RedisModuleString对象,很有必要了解RedisModuleString的相关操作;

RedisModuleString对象跟char*和len转换:

const char *RedisModule_StringPtrLen(RedisModuleString *string, size_t *len);

上面这个函数可以将RedisModuleString对象转换成char和len,但是返回的char是const的;

也可以直接使用char指针和长度参数创建RedisModuleString对象:

RedisModuleString *RedisModule_CreateString(RedisModuleCtx *ctx, const char *ptr, size_t len);

但是直接创建的RedisModuleString对象必须使用RedisModule_FreeString()函数释放(在没有使用自动内存管理的时候):

void RedisModule_FreeString(RedisModuleString *str);

另外,参数argv的字符串对象永远不需要释放,其他api返回的字符串需要释放的情况会有明确说明;

RedisModuleString对象跟数字转换:

将数字转换成字符串对象可以使用:

RedisModuleString *str= RedisModule_CreateStringFromLongLong(ctx,12345);

将字符串转换成数字可以使用:

long long x;

if (RedisModule_StringToLongLong(str, &x) == REDISMODULE_OK) {

}

访问redis数据:

可以使用高级API或者低级API两种方式操作redis,低级API速度可以媲美原生redis命令,但是大部分执行时间可能是用在处理数据而不是操作redis数据,所以高级API也并不是多余的;(高级API不会被单独做成模块的API,可以通过模块提供的一个RedisModule_Call()函数去使用高级API,就像Lua脚本那样)

使用高级API访问redis数据:

高级API使用的函数:

RedisModuleCallReply *RedisModule_Call(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...);

参数1是上下文,参数2是需要调用的redis命令,参数3是对后面参数的格式说明(每个字符分别对应后面的参数,格式!、A、3、R是没有对应参数),后面参数为提供给命令的参数;

格式说明符:

  • b:表示缓冲区,并且后面紧跟一个参数表示缓冲区长度;
  • c:表示指向c字符串的指针(以空字符结尾的字符串);
  • l:表示一个long long整型;
  • s:表示RedisModuleString对象;
  • v:表示一个装有RedisModuleString对象的vector向量;
  • !:说明将命令和参数发送给副本和AOF文件;
  • A:说明只将命令和参数发送给副本,不写入AOF文件;(需搭配!)
  • R:说明只将命令和参数写入AOF文件,不发送到副本;(需搭配!)
  • 3:说明返回一个RESP3的应答,这将改变命令的应答;(例如HGETALL命令将返回一个map而不是扁平数组)
  • 0:说明以自动模式返回一个应答,直接将应答返回给客户端;
  • C:根据ACL规则检查命令是否可执行;

命令执行成功返回一个RedisModuleCallReply对象,失败返回NULL并且将errno设置为以下值:

  • EBADF:错误格式说明符;
  • EINVAL:错误的参数数量;
  • ENOENT:命令不存在;
  • EPERM:集群模式中使用的key的槽位不在本地redis上;
  • EROFS:集群模式中以只读状态发送了写命令;
  • ENETDOWN:集群已经关闭;
  • ENOTSUP:没有指定模块上下文的ACL用户;
  • EACCES:根据ACL规则无法执行命令;

使用RedisModuleCallReply接收响应:

RedisModuleCallReply对象可以使用以RedisModule_CallReply为前缀的函数访问;

可以使用RedisModule_CallReplyType()函数查看reply对象的类型:

int RedisModule_CallReplyType(RedisModuleCallReply *reply);

函数返回的reply对象的类型有:(api官网定义了好多,h头文件只定义了6个,下划线那些)

  • REDISMODULE_REPLY_UNKNOWN
  • REDISMODULE_REPLY_STRING
  • REDISMODULE_REPLY_ERROR
  • REDISMODULE_REPLY_INTEGER
  • REDISMODULE_REPLY_ARRAY
  • REDISMODULE_REPLY_NULL
  • REDISMODULE_REPLY_MAP
  • REDISMODULE_REPLY_SET
  • REDISMODULE_REPLY_BOOL
  • REDISMODULE_REPLY_DOUBLE
  • REDISMODULE_REPLY_BIG_NUMBER
  • REDISMODULE_REPLY_VERBATIM_STRING
  • REDISMODULE_REPLY_ATTRIBUTE

可以使用RedisModule_CallReplyLength()函数计算reply对象的长度:

size_t RedisModule_CallReplyLength(RedisModuleCallReply *reply);

对于字符串或者错误类型计算的是字符串的长度,对于数组计算的是数组里元素的数量;

获取整数reply的值可以使用RedisModule_CallReplyInteger()函数:

long long RedisModule_CallReplyInteger(RedisModuleCallReply *reply);

使用错误类型的reply对象调用上面的函数总是返回LLONG_MIN值;

访问数组元素可使用RedisModule_CallReplyArrayElement()函数:

RedisModuleCallReply *RedisModule_CallReplyArrayElement(RedisModuleCallReply *reply, size_t idx);

访问数组元素的时候如果越界则返回NULL;

对于string或者error类型的reply可以使用以下函数转换:

const char *RedisModule_CallReplyStringPtr(RedisModuleCallReply *reply, size_t *len);

len需要先定义用来接收转换成char的长度,另外char是const修饰的,如果reply不是string或者error类型的则返回NULL;

还可以通过RedisModule_CreateStringFromCallReply()函数直接使用reply创建RedisModuleString对象(通常用于将字符串或者整数的reply传递给其他api使用):

RedisModuleString *RedisModule_CreateStringFromCallReply(RedisModuleCallReply *reply);

该函数可以处理string、error、integer的reply,如果reply类型错误则返回NULL,如果不使用自动内存管理,则需要RedisModule_FreeString()函数释放创建的字符串;

注意:reply对象必须使用void RedisModule_FreeCallReply(RedisModuleCallReply *reply);函数释放;对于数组类型的reply只需要释放最上层的reply即可,不需要释放嵌套的reply;如果使用自动内存管理则可以不用释放reply(为了尽快释放内存也可以主动释放reply);

返回结果给客户端:

模块实现的新命令必须能够返回值给调用者;(错误可以与任何错误字符串和错误码返回,错误码是错误消息中开头的大写单词)

向客户端发送应答的函数都是以RedisModule_ReplyWith开头的函数,都在h头文件里:

REDISMODULE_API int (*RedisModule_ReplyWithLongLong)(RedisModuleCtx *ctx, long long ll) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithError)(RedisModuleCtx *ctx, const char *err) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithSimpleString)(RedisModuleCtx *ctx, const char *msg) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithArray)(RedisModuleCtx *ctx, long len) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithNullArray)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithEmptyArray)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithStringBuffer)(RedisModuleCtx *ctx, const char *buf, size_t len) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithCString)(RedisModuleCtx *ctx, const char *buf) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithString)(RedisModuleCtx *ctx, RedisModuleString *str) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithEmptyString)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithVerbatimString)(RedisModuleCtx *ctx, const char *buf, size_t len) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithNull)(RedisModuleCtx *ctx) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithDouble)(RedisModuleCtx *ctx, double d) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithLongDouble)(RedisModuleCtx *ctx, long double d) REDISMODULE_ATTR;
REDISMODULE_API int (*RedisModule_ReplyWithCallReply)(RedisModuleCtx *ctx, RedisModuleCallReply *reply) REDISMODULE_ATTR;

另外h头文件里只定义了一个参数类型错误的错误类型:

返回数组需要先使用RedisModule_ReplyWithArray()函数设定一个数组长度,然后再返回每个元素即可:

返回嵌套数组可以这样:

关于创建数组时未知大小的动态数组,可以先通过RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN)函数创建数组(REDISMODULE_POSTPONED_ARRAY_LEN为特殊参数,h头文件里定义为-1),然后设置数组的值,最后再根据设定的值使用RedisModule_ReplySetArrayLength()函数指定数组长度即可,就像这样:

嵌套的动态数组:

使用低级API访问redis数据:

使用低级api可以直接操作跟键相关的值对象,执行速度类似于redis内部命令的速度;

低级api使用过程:通过打开一个键获得键指针,使用键指针进行低级api调用,最后关闭键;

由于低级api是非常快的,所以不能执行过多耗时操作,需要遵循一些规则:

  • 当键被打开进行写入时,多次打开同一个键是未定义的,可能引发崩溃;
  • 当一个键被打开后,只能通过低级api进行操作,此时使用高级api操作同一个键会引发崩溃,低级api和高级api不能同时在同一个键上使用,打开键使用低级api再关闭后才可以使用其他api;

可以使用RedisModule_OpenKey()函数打开一个键获得键指针:

void *RedisModule_OpenKey(RedisModuleCtx *ctx, robj *keyname, int mode);

参数2是键名,必须是RedisModuleString对象,参数3是打开键的模式REDISMODULE_READ和REDISMODULE_WRITE(可以使用“|”符号连接起来同时使用两种模式,虽然写模式默认包含读模式)

对于不存在的键仍然可以打开,使用写模式打开会新建键并返回键指针,使用读模式打开只会返回NULL;(在NULL值上调用RedisModule_CloseKey()和RedisModule_KeyType()是安全的)

打开的键必须进行关闭,使用关闭函数:

void RedisModule_CloseKey(RedisModuleKey *key);

如果启用了自动内存管理,可以不需要手动关闭键,redis会小心的自动关闭仍然打开的键;

获得键的类型可以通过RedisModule_KeyType()函数:

int RedisModule_KeyType(RedisModuleKey *key);

如果参数是一个不存在的NULL键指针,则会返回REDISMODULE_KEYTYPE_EMPTY;

头文件里定义的键类型:

#define REDISMODULE_KEYTYPE_EMPTY 0
#define REDISMODULE_KEYTYPE_STRING 1
#define REDISMODULE_KEYTYPE_LIST 2
#define REDISMODULE_KEYTYPE_HASH 3
#define REDISMODULE_KEYTYPE_SET 4
#define REDISMODULE_KEYTYPE_ZSET 5
#define REDISMODULE_KEYTYPE_MODULE 6
#define REDISMODULE_KEYTYPE_STREAM 7

创建键,只需要以写模式打开一个不存在的键,然后使用一个键写入函数即可;

删除键可使用删除函数:

int RedisModule_DeleteKey(RedisModuleKey *key);

如果以写模式打开了键,则会删除该键并返回REDISMODULE_OK,否则返回REDISMODULE_ERR(包括读模式打开),键被删除后变成空键,使用键写入后会变成写入的那种类型;

关于键的过期:

查询键过期时间函数:

mstime_t RedisModule_GetExpire(RedisModuleKey *key);

返回键过期剩余的毫秒值,没有设置过期或者键不存在返回REDISMODULE_NO_EXPIRE(可以判断键类型REDISMODULE_KEYTYPE_EMPTY区分);

设置键的过期时间函数:

int RedisModule_SetExpire(RedisModuleKey *key, mstime_t expire);

设置键的过期时间(单位毫秒),过期时间需要是一个正整数,键要以写模式打开,成功返回REDISMODULE_OK,对不存在的键设置时返回REDISMODULE_ERR,如果当前键还未过期则设置一个新的过期时间,如果已过期则替换为新值;可以设置一个特殊值REDISMODULE_NO_EXPIRE来取消过期时间(相当于PERSIST命令);

另外6.2.6版本新增了两个函数,使用Unix时间戳作为单位的函数:

mstime_t RedisModule_GetAbsExpire(RedisModuleKey *key);

int RedisModule_SetAbsExpire(RedisModuleKey *key, mstime_t expire);

获得值的长度:

计算键的值的长度使用函数:

size_t RedisModule_ValueLength(RedisModuleKey *key);

对于字符串返回字符串的长度,其他类型则返回元素的数量(仅计算散列的键值),键不存在返回0;

详细的各种类型的操作请参见官方API文档:Modules API reference – Redis

关于复制命令到副本或者AOF:

高级API可以使用RedisModule_Call()函数,格式说明符里使用“!”“A”“R”控制复制到副本或者AOF里;

低级API可以使用以下函数复制命令到slave和aof里:

int RedisModule_ReplicateVerbatim(RedisModuleCtx *ctx);

还可以使用专用函数复制:

int RedisModule_Replicate(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...);

该函数的参数跟RedisModule_Call()的参数一致,也需要格式说明符,可以使用“A”或“R”控制在副本或者AOF中传播;

在模块里申请释放内存:

c语言提供了malloc()和free()来申请和释放内存,虽然redis没有禁止c语言的内存管理,但是redis模块也提供了代替他们的内存管理的函数,c语言的内存分配对于redis是透明的,另外从rdb反序列化加载自定义本地类型的模块数据时可以直接填充使用RedisModule_Alloc()分配的内存空间,而不需要复制到数据结构中:

void *RedisModule_Alloc(size_t bytes);

该函数就像malloc()函数一样,但是这个函数分配的内存会在redis的INFO内存信息中体现出来,会被认为是redis占用的内存,也会根据最大内存设置影响键的驱逐;

void *RedisModule_Calloc(size_t nmemb, size_t size);

该函数类似calloc()函数,跟上一个函数基本一样;

void* RedisModule_Realloc(void *ptr, size_t bytes);

类似realloc(),也是使用RedisModule_Alloc()重新分配内存;

void RedisModule_Free(void *ptr);

类似free(),释放RedisModule_Alloc()和RedisModule_Realloc()申请的内存,永远不要用来释放malloc()申请的内存;

char *RedisModule_Strdup(const char *str);

类似strdup(),复制字符串,内部使用RedisModule_Alloc()分配的内存;

对于短生命周期的小内存的申请可以使用池分配器分配:

void *RedisModule_PoolAlloc(RedisModuleCtx *ctx, size_t bytes);

该函数返回堆分配的内存,在模块回调函数返回的时候将自动释放内存,并且可以自动字节对齐,如果bytes为0则返回NULL;

构造自定义本地数据类型:

本地类型由以下几块内容构成:(源码redis-6.2.6\src\modules\hellotype.c是一个很好的例子)

  • 新类型的结构体定义以及对新类型的新的操作方法;
  • 一组必要函数:保存RDB、读取RDB、AOF重写、释放资源;
  • 一个由9个字符组成的唯一的类型名;
  • 一个编码版本号,用于RDB加载;

注册新的本地类型:

注册新的本地类型首先需要定义一个RedisModuleType的全局指针变量,用于保存注册数据类型后的引用(本地类型的结构体定义在注册这里暂时用不到,在需要实现的方法里会用到);

然后需要在RedisModule_OnLoad()函数里实例化一个RedisModuleTypeMethods对象,该变量已在头文件里定义:

api官网的例子:(多了几个扩展项)

其中version、rdb_load、rdb_save、aof_rewrite、free是通常需要设置的变量和实现的方法,其他为可选;

  • version:通常是头文件里定义的REDISMODULE_TYPE_METHOD_VERSION变量;
  • rdb_load:从RDB文件加载的时候调用,用于说明如何将rdb里的该类型数据加载到内存,数据格式应该跟rdb_save格式对应;
  • rdb_save:将数据保存到RDB文件的时候调用,用于说明如何将本地类型保存到rdb文件里;
  • aof_rewrite:AOF文件被重写的时候调用,用于说明重新创建键的内容的命令序列;
  • free:当本地类型被各种原因删除的时候调用,用于回收跟该键值有关的内存;

这四个函数在头文件里的定义:

typedef void *(*RedisModuleTypeLoadFunc)(RedisModuleIO *rdb, int encver);

typedef void (*RedisModuleTypeSaveFunc)(RedisModuleIO *rdb, void *value);

typedef void (*RedisModuleTypeRewriteFunc)(RedisModuleIO *aof, RedisModuleString *key, void *value);

typedef void (*RedisModuleTypeFreeFunc)(void *value);

然后调用RedisModule_CreateDataType()函数创建本地类型:

moduleType *RedisModule_CreateDataType(RedisModuleCtx *ctx, const char *name, int encver, void *typemethods_ptr);

参数1是上下文,参数2是9个字符的自定义唯一类型名(使用A-Z、a-z、0-9、_和-字符),参数3是持久化版本号(encver必须为0 ~ 1023之间的正值,rdb_load函数可以根据该值使用不同版本的加载方法),参数4是上面的RedisModuleTypeMethods对象;返回值使用最开始定义的RedisModuleType全局变量指针保存即可,如果名字重复或者encver无效则会返回NULL;另外注意“AAAAAAAAA”类型名被保留并产生一个错误;

关于类型名必须为9个字符的原因:

持久化RDB文件的时候使用的是键值对,格式:[1 byte type] [key] [a type specific value],其中1个byte存储字符串、列表、集合等类型,模块数据为一个特定值,无法表示某个具体数据类型只能说明为模块数据,所以在type specific value中加入一个64位的整数前缀解释具体的数据类型,类型名由A-Za-z0-9下划线和减号组成(64个符号的字符集),6个bit刚好能保存一个字符,9个字符需要54个bit,剩下10个bit保存持久化版本号encver(这也说明了encver范围是0-1023,2^10一共能保存1024个数),最后这个64位的整数格式:6|6|6|6|6|6|6|6|6|10;可以直接通过这个整数获得类型名和版本号,出错的时候也可以直接将这个类型名返回给客户端,另外使用TYPE命令的时候返回的也是这个类型名;

另外注册了本地类型的模块无法卸载:

RDB保存和加载:

redis提供了高级API用于存取rdb数据,可以方便的存取以下数据类型:

  • Unsigned 64 bit integers
  • Signed 64 bit integers
  • String
  • StringBuffer
  • Double
  • Float
  • LongDouble

用于保存这些数据类型的函数:

void RedisModule_SaveUnsigned(RedisModuleIO *io, uint64_t value);

void RedisModule_SaveSigned(RedisModuleIO *io, int64_t value);

void RedisModule_SaveString(RedisModuleIO *io, RedisModuleString *s);

void RedisModule_SaveStringBuffer(RedisModuleIO *io, const char *str, size_t len);

void RedisModule_SaveDouble(RedisModuleIO *io, double value);

void RedisModule_SaveFloat(RedisModuleIO *io, float value);

void RedisModule_SaveLongDouble(RedisModuleIO *io, long double value);

对应的用于获取这些数据类型的函数:

uint64_t RedisModule_LoadUnsigned(RedisModuleIO *io);

int64_t RedisModule_LoadSigned(RedisModuleIO *io);

RedisModuleString *RedisModule_LoadString(RedisModuleIO *io);

char *RedisModule_LoadStringBuffer(RedisModuleIO *io, size_t *lenptr);

double RedisModule_LoadDouble(RedisModuleIO *io);

float RedisModule_LoadFloat(RedisModuleIO *io);

long double RedisModule_LoadLongDouble(RedisModuleIO *io);

这些读写API不需要模块进行错误检查,模块总是认为成功;rdb_load函数是用来将储存在RBD文件中的数据重新构建出来,虽然API上没有错误处理,如果读取的内容有问题rdb_load函数在出错时仍可以返回NULL,redis仅仅是不知道发生了什么;

另外还有一个检测错误的函数:

int RedisModule_IsIOError(RedisModuleIO *io);

如果之前调用的任何IO API失败,该函数会返回true,对于RedisModule_Load*的api,需要先通过RedisModule_SetModuleOptions()函数设置REDISMODULE_OPTIONS_HANDLE_IO_ERRORS标志;

AOF重写:

对于aof重写只有一个函数:

void RedisModule_EmitAOF(RedisModuleIO *io, const char *cmdname, const char *fmt, ...);

用于在AOF重写过程中向AOF发出命令。这个函数只在模块自定义的数据类型的aof_rewrite函数中被调用。工作方式和RedisModule_Call()一样,参数传递方式也一样,但该函数无返回值;

Free函数:

当redis需要释放一个保存了本地类型的键时,需要模块提供一个释放函数来帮助处理内存;

如果本地数据类型只是一个简单的分配内存组成,可以直接在free函数里使用一个释放函数:

void RedisModule_Free(void *ptr);

通常数据类型更复杂,需要将free函数里的void指针参数转换成对应数据类型,然后释放结构内的其他所有内存资源;

读写数据:

构造好了本地数据类型,需要提供各种使用方法,类似get、set等方法,通常使用低级api操作存取数据;

保存本地类型数据使用RedisModule_ModuleTypeSetValue()函数:

int RedisModule_ModuleTypeSetValue(RedisModuleKey *key, moduleType *mt, void *value);

参数1是以写模式打开的key指针,参数2是本地类型的引用(注册时获得的保存在全局里的RedisModuleType指针),参数3是包含具体本地数据的指针(自定义的struct结构体数据);

如果有旧值则被覆盖,成功返回REDISMODULE_OK,否则返回REDISMODULE_ERR;

读取本地类型数据使用RedisModule_ModuleTypeGetValue()函数:

void *RedisModule_ModuleTypeGetValue(RedisModuleKey *key);

参数1是打开的key指针;

如果该key通过RedisModule_KeyType(key)函数返回类型是REDISMODULE_KEYTYPE_MODULE,则会按照RedisModule_ModuleTypeSetValue()时传入的结构体返回结构体指针,如果key为NULL或者类型不是模块类型或者是空的,则该函数返回NULL;

另外还可以通过RedisModule_ModuleTypeGetType()函数获得模块具体类型:(RedisModule_KeyType()函数只会返回REDISMODULE_KEYTYPE_MODULE)

moduleType *RedisModule_ModuleTypeGetType(RedisModuleKey *key);

返回值指的是全局定义的那个RedisModuleType指针变量(注册时保存了类型引用);

官网的惯用检查代码展示:

关于内存分配:

模块数据类型应该使用RedisModule_Alloc()函数家族来分配内存和释放内存,使用redis提供的内存分配函数有许多好处:

  • redis可以将内存分配统计到内存占用里;
  • redis使用jemalloc分配内存,可以避libc引起的碎片问题;
  • rdb加载的时候可以直接使用RedisModule_Alloc()函数分配内存,直接将内存连接到数据结构中,避免数据拷贝;

如果模块里使用了libc分配内存或者使用了libc分配内存的库,可以使用简单的宏来将libc替换成redis的api:

#define malloc RedisModule_Alloc
        #define realloc RedisModule_Realloc
        #define free RedisModule_Free
        #define strdup RedisModule_Strdup

但是应注意libc和redis api不能混用,否则会导致redis异常或者崩溃,例如不能使用RedisModule_Free()释放libc分配的内存;

一个本地数据类型的小demo:

test.c代码:

#include "redismodule.h"

//全局的RedisModuleType指针
static RedisModuleType *Mytype = NULL;
/*
int MyCmd(RedisModuleCtx *ctx, RedisModuleString ** argv, int argc)
{
	RedisModule_ReplyWithSimpleString(ctx, "TEST_OK");
	return REDISMODULE_OK;
}
*/
//定义具体类型的结构体
typedef struct{
	long long id;
	RedisModuleString *name;
} MYTYPE;
//从rdb加载数据
void *MytypeLoad(RedisModuleIO *io, int encver)
{
	long long id = RedisModule_LoadSigned(io);
	if (RedisModule_IsIOError(io)) return NULL;
	RedisModuleString *name = RedisModule_LoadString(io);
	if (RedisModule_IsIOError(io)) return NULL;
	MYTYPE *mt = (MYTYPE *)RedisModule_Alloc(sizeof(MYTYPE));
	mt->id = id;
	mt->name = name;
	return mt;
}
//保存数据到rdb文件
void MytypeSave(RedisModuleIO *io, void *value)
{
	MYTYPE *mt = (MYTYPE *)value;
	RedisModule_SaveSigned(io, mt->id);
	RedisModule_SaveString(io, mt->name);
}
//aof rewrite的时候调用
void MytypeRewrite(RedisModuleIO *aof, RedisModuleString *key, void *value)
{
	//未生效,未知原因
	MYTYPE *mt = (MYTYPE *)value;
	if (mt)
	{
		RedisModule_EmitAOF(aof, "mtset", "sls", key, mt->id, mt->name);
	}
}
//删除键的时候释放资源
void MytypeFree(void *value)
{
	if (value)
	{
		MYTYPE * mt = (MYTYPE *)value;
		if (mt->name) RedisModule_FreeString(NULL, mt->name);
		RedisModule_Free(mt);
	}
}
//保存数据
int MytypeSet(RedisModuleCtx *ctx, RedisModuleString ** argv, int argc)
{
	if (argc != 4)
	{
		return RedisModule_WrongArity(ctx);
	}
	RedisModule_AutoMemory(ctx);
	//判断参数	
	long long argid;
	if (RedisModule_StringToLongLong(argv[2], &argid) != REDISMODULE_OK)
	{
		return RedisModule_ReplyWithError(ctx, "ERROR 2nd param is not a Integer");
	}
	//判断key
	RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ|REDISMODULE_WRITE);
	int type = RedisModule_KeyType(key);
	if (type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(key) != Mytype)
	{
		return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
	}
	//保存数据和返回结果
	MYTYPE *data = RedisModule_Calloc(sizeof(MYTYPE), 1);
	data->id = argid;
	data->name = argv[3];
	RedisModule_RetainString(ctx, data->name);
	//保存
	RedisModule_ModuleTypeSetValue(key, Mytype, data);
	RedisModule_CloseKey(key);
	RedisModule_ReplyWithSimpleString(ctx, "OK");

	return REDISMODULE_OK;
}
//读取数据
int MytypeGet(RedisModuleCtx *ctx, RedisModuleString ** argv, int argc)
{
	if (argc != 2)
	{
		return RedisModule_WrongArity(ctx);
	}
	RedisModule_AutoMemory(ctx);
	RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_READ|REDISMODULE_WRITE);
	int type = RedisModule_KeyType(key);
	if (type != REDISMODULE_KEYTYPE_EMPTY && RedisModule_ModuleTypeGetType(key) != Mytype)
	{
		return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE);
	}
	//读取
	MYTYPE *data = RedisModule_ModuleTypeGetValue(key);	
	RedisModule_CloseKey(key);
	if (!data)
	{
		RedisModule_ReplyWithNullArray(ctx);
		//RedisModule_ReplyWithNull(ctx);
	}
	else
	{
		//使用数组返回自定义数据
		RedisModule_ReplyWithArray(ctx, 2);
		RedisModule_ReplyWithLongLong(ctx, data->id);
		RedisModule_ReplyWithString(ctx, data->name);
		//简单构造一个json字符串返回
		//size_t len;
		//const char *n = RedisModule_StringPtrLen(data->name, &len);
		//RedisModuleString * jsonstr = RedisModule_CreateStringPrintf(ctx, "{'id':%d, 'name':'%s'}", data->id, n);
		//RedisModule_ReplyWithString(ctx, jsonstr);
	}
	return REDISMODULE_OK;
}

int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv,int argc)
{
	if (RedisModule_Init(ctx, "mymodule", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR)
	{
		return REDISMODULE_ERR;
	}
	//为RedisModule_Load*一类的函数启用RedisModule_IsIOError()	
	RedisModule_SetModuleOptions(ctx, REDISMODULE_OPTIONS_HANDLE_IO_ERRORS);
	//指定类型必需的方法	
	RedisModuleTypeMethods tm = {
		.version = REDISMODULE_TYPE_METHOD_VERSION,
		.rdb_load = MytypeLoad,
		.rdb_save = MytypeSave,
		.aof_rewrite = MytypeRewrite,
		.free = MytypeFree,
	};

	//创建自定义类型,类型名字必须9个字符
	Mytype = RedisModule_CreateDataType(ctx, "Mytype789", 1, &tm);
	if (Mytype == NULL)
	{
		return REDISMODULE_ERR;
	}
	if (RedisModule_CreateCommand(ctx, "mtset", MytypeSet, "write", 1, 1, 1) == REDISMODULE_ERR)
	{
		return REDISMODULE_ERR;
	}
	if (RedisModule_CreateCommand(ctx, "mtget", MytypeGet, "readonly", 1, 1, 1) == REDISMODULE_ERR)
	{
		return REDISMODULE_ERR;
	}
/*	
	if (RedisModule_CreateCommand(ctx, "mycmd", MyCmd, "readonly", 0, 0, 0) == REDISMODULE_ERR)
	{
		return REDISMODULE_ERR;
	}
*/
	return REDISMODULE_OK;
}

生成一个test.so文件:

另外修改一下redis.conf配置,使redis启动的时候就加载模块并开启rdb持久化用来测试:

loadmodule test.so

save 5 1

启动redis服务:

已经loaded模块了,使用redis-cli连接,并存取几个数据:

关闭redis自动保存rdb,然后重新启动redis加载rdb文件测试:

重新连接redis,读取数据:

可以看到rdb里也保存了自定义的本地数据类型;

另外aof_rewrite不知道为啥不好用,aof持久化没鼓捣好;

=。=写完博客一刷新官网,官网居然换新版了,左右两侧还多了目录,网址也换新了:

Redis modules API | Redis

Modules API reference | Redis

实验性的阻塞命令以后有时间再研究;

参考:Redis Modules: an introduction to the API – Redis

redis 4.0以上的module (一)_gochenguowei的博客-CSDN博客_redismodule

相关文章