redis 4.0 扩展模块介绍

加载模块

加载模块方法
  • 方法1 在redis.conf 中增加 loadmodule /path/to/mymodule.so
  • 方法2 使用MODULE LOAD /path/mymodule.so 命令去加载
读取模块列表命令
  • MODULE LIST
卸载模块命令
  • MODULE UNLOAD mymodule
    模块的动态库的命名应该和模块一个名字这是一个很好的习惯

module demo

为了能更好的展示一个模块的不同组成部分,在本文将实现一个返回一个随机数的命令的模块

#include "redismodule.h"
#include <stdlib.h>

int HelloworldRand_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    RedisModule_ReplyWithLongLong(ctx,rand());
    return REDISMODULE_OK;
}

int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    if (RedisModule_Init(ctx,"helloworld",1,REDISMODULE_APIVER_1)
        == REDISMODULE_ERR) return REDISMODULE_ERR;

    if (RedisModule_CreateCommand(ctx,"helloworld.rand",
        HelloworldRand_RedisCommand) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    return REDISMODULE_OK;
}

上面的俩个函数,第一个函数是我们自定义命令(模块)的一个函数,第二个函数是RedisModule_Onload是一个必须出现在每个Redis module中的一个函数,这个函数里面要对一个指向redis module 的一个指针进行初始化,并且对其注册命令、可能被使用到的私有结构体。
注意我们给一个命令的命名风格是 module.command 。这样就不太可能出现命令重名的情况,如果不同的模块有相同命名的命令Redis会报错,因为一个 RedisModule_CreateCommand 函数将会在其中一个模块的创建命令的过程中失败,所以模块加载将会返回 REDISMODULE_ERR 的错误码

Module 初始化

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

上面这个函数应该是在OnLoad函数中第一个被调用的,它用来告诉Redis内核我们要注册的module的module_name 、module_version、api_version。
如果有API版本错误、模块name已存在、其他类似的错误话,这个函数将返回REDISMODULE_ERR,并且在OnLoad函数中必须把这个错误返回出去。
注意如果这个函数不是第一个调用的那么会发生段错误导致redis崩溃。

RedisModule_CreateCommand

这个函数用来注册一个命令在Redis内核中

int RedisModule_CreateCommand(RedisModuleCtx *ctx, const char *cmdname,RedisModuleCmdFunc cmdfunc);

正如你所看到的,大多数Redis 模块调用的API都有第一个参数 context ,以至于对创建命令、执行命令的时候都需要一个这样的参数
为了在上面讲的 ctx中创建一个命令,我们需要一个ctx、命令名、函数指针。这个函数指针的类型是RedisModuleCmdFunc

RedisModuleCmdFunc

该函数的第一个参数也是ctx,它将被相应的其他API调用它时传递,其他的俩个命令行参数由用户传递。就像下面的原型所展示的出的,第二个参数类型是一个特殊的string的数组。对于这个RedisModuleString来讲,它是一个特殊的数据类型,我们只能通过API去访问、使用它,绝对不能直接去访问或者修改它的内容。

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

回顾到上面初始化命令时的Load函数中,还有一个RedisModule_ReplyWithLongLong函数,该函数只是返回一个整形给调用这个命令的client,事实上其他Redis命令也是这个原理,比如 INCR or SCARD

传递配置参数给Redis module

当一个Redis module被MODULE LOAD 命令加载时或使用 loadmodule 直接在redis.conf文件中加载时,用户是能够传递配置参数给相应的模块通过在加载一个模块时在后面追加参数,列如

loadmodule mymodule.so  foo bar 1234

在上面的列子中,string 类型的 boo、bar 与 123将被传递给 OnLoad函数在 argv数组中,它们的参数个数就是argc。这些命令行参数都会被保存在一个static 的全局变量中,它可以在该模块中被广泛的访问,以至于可以起到相同的参数能够在不同的命令中有不同的表现

如何使用RedisModuleString

这个特殊的string类型,通常被传递给自定义的commands函数中,并且也有可能其他module的返回值也是这个类型
通常上我们都是直接传递这个string给其他module API,然后有时你也需要去直接访问这个string对象,所以下面介绍一些操作该string对象的函数

读取RedisModuleString

可以看到下面的函数原型,它返回的是一个const 类型的指针,所以我们只能去读这个 RedisModuleString中的内容但是绝对不能修改它

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

如果你想去创建一个新的string 对象你可以用下面的API,它返回一个RedisModuleString对象,我们必须记得析构它因为它是在堆上申请的内存,否则会造成内存泄漏。如果我们要释放它,得使用相应的RedisModule_FreeString函数

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

如果你想去使用RAII的方式去管理这个string,可以使用Redis提供的相应函数,下面会介绍到。
注意命令行参数的string是绝对不能释放的,我们只能free我们创建的string 或者 一些API标注了它返回的string必须要释放的那些string。

void RedisModule_FreeString(RedisModuleString *str);
RedisModuleString版本的 atoi 或 itoa

我们大多数时候都有需要把 整形转化为一个string的需求,那么我们可以通过下面这个函数来实现

RedisModuleString *mystr = RedisModule_CreateStringFromLongLong(ctx,10);

相似地也可以把string 转换为一个整形

RedisModule_StringToLongLong(RedisModuleCtx * ctx,RedisModuleString ** argv ,int argc)
long long myval;
if (RedisModule_StringToLongLong(ctx,argv[1],&myval) == REDISMODULE_OK) {
    /* Do something with 'myval' */
}
论使用 modules 访问 Redis key

大多数自定义模块为了便用性,都不得不直接访问Redis 的数据。Redis 模块 有俩套 API 可以访问Redis数据,一套是低级API,一套是高级API。低级API提供更快的访问和一系列操作Redis数据结构的函数接口。高级API则通过使用Redis命令去得到相应结果,它与Lua 访问 Redis很类型,但是高级API也是很有用的,它可以访问低级API不可访问的Redis功能。
通常上模块开发者更喜欢低级API,因为命令实现时使用低级API可以达到原生命令的运行速度。但是也有明确的高级API使用场景比如有时一个需求的瓶颈不在访问速度而在于处理速度。但是也要记住,有时使用低级API不比高级API难。

高级API

高级API实现Redis commands

高级API 是由一系列的RedisModule_Call 和 访问reply对象函数构成的一个集合。
RedisModule_Call 有一个特殊的参数格式,这个参数格式通过可变参数来表示出传递给RedisModule_Call函数的都是什么参数类型,RedieModule可接受的参数类型有 C风格字符串、RedisModuleString、 带有len的二进制的Cbuffer、longlong。
当我们想调用 INCRBY 指令时它接受的第一个参数就是先前已经收到的一个命令行参数(这个指已经设置过的key),第二个参数是一个C风格的number字符串,如下

[yutian@localhost src]$ ./redis-cli 
127.0.0.1:6379> set num 10
OK
127.0.0.1:6379> get num
"10"
127.0.0.1:6379> INCRBY num 20
(integer) 30
127.0.0.1:6379> get num
"30"

对于RedisModuleCallReply的INCRBY如下它的第一个参数是 module context ,第二个是个C风格的字符串表示命令名,第三个是参数格式,剩下俩个是自定义指定的来个参数,其中一个是RedisModuleString 一个是C风格字符串10

RedisModuleCallReply *reply;
reply = RedisModule_Call(ctx,"INCR","sc",argv[1],"10");
RedisModule_Call
RedisModuleCallReply * RedisModule_Call(RedisModuleCtx * ctx , const char * command , 
const char * format,char * arg,...);
RedisModule_Call的参数格式
  • c : c风格字符串(以\0结尾的字符串)
  • b : c buffer ,它是一个c的string对象和一个size_t 俩部分组成
  • s : RedisModuleString 通常是由其他Redis module API返回的一个对象
  • l : longlong 整形
  • v : 一个RedisModuleString的数组
  • !: 它不是类型,只是用来表示该命令同步到 slaves 和 AOF的。

如果成功返回 RedisModuleCallReply对象指针,失败返回NULL。

EINVAL  参数格式字符串无法失败 或 参数个数错误 或命令名错误
EPERM  在开启了Cluster模式,当目标key没有对应的hash clot (hash 槽)的时候返回该错误
使用RedisModuleCallReply对象
获取RedisModuleCallReply对象的类型
RedisModule_CallReplyType(RedisModuleCallReply * reply);

该函数可以获得Reply对象的类型
合法的 reply type

  • REDISMODULE_REPLY_STRING 大部分string
  • REDISMODULE_REPLY_ERROR error
  • REDISMODULE_REPLY_INTEGER 64bit int
  • REDISMODULE_REPLY_ARRAY 虽然是一个reply 对象,但是内部封装的是reply 数组,这个类型就是代表数组类型
  • REDISMODULE_REPLY_NULL 代表 NULL 的reply
reply = RedisModule_Call(ctx,"INCR","sc",argv[1],"10");
if (RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_INTEGER) {
    long long myval = RedisModule_CallReplyInteger(reply);
    /* Do something with myval. */
}
RedisModule_CallReplyLength

对于string 、error、array它们三个类型都是有相应的长度的如下是获取长度的函数

size_t reply_len = RedisModule_CallReplyLength(reply);
RedisModule_CallReplyInteger

获取 int 类型的Reply的值,当传递的Reply类型不正确返回 LLONG_MIN 错误

long long reply_integer_val = RedisModule_CallReplyInteger(reply);
RedisModule_CallReplyArrayElement

访问Reply 数组的元素,如果返回NULL则表示越界

RedisModuleCallReply *subreply;
subreply = RedisModule_CallReplyArrayElement(reply,idx);
RedisModule_CallReplyStringPtr

获取string 和 error的类型的结果,注意我们不能修改返回的这个字符串,如果传入类型不对返回NULL

size_t len;
char *ptr = RedisModule_CallReplyStringPtr(reply,&len);
RedisModule_CreateStringFromCallReply

用来把error、int、string类型的Reply转换为一个 Redis Module string , 这个返回的string 必须通过RedisModule_FreeString 函数来释放

RedisModuleString *mystr = RedisModule_CreateStringFromCallReply(myreply)
论释放RedisModuleCallReply对象

  所有API返回的Reply 对象必须都得通过 RedisModule_FreeCallReply释放。对于Reply的数组类型,我们只能释放最外层的reply而不是内层的reply。虽然目前module实现了一个保护机制,当我们去释放内层reply对象的话它会有相应的机制避免程序崩溃,但是我们不能依赖于这个机制所以不能把这个机制当成RedisModule_FreeCallReply函数的一部分。
  如果你使用了自动内层管理,你就不需要每次都去释放API返回的Reply对象

对于外层reply 和 内层reply对象的解释
RedisModuleCallReply * nestedreply;
nestedreply =  RedisModule_CallReplyArrayElement(OutReply,idx);
向client 返回结果

  像一个正常的Redis命令一样,我们通过module实现的 Redis命令也必须给client返回一个返回值。Redis Module实现了一系列函数给我们,让我们可以用它们去实现向client返回一个属于Redis 协议的结果(原文 array of Redis protocol types as elemented)。error 也能够返回一个相应带有错误信息的string 和 错误码。
  所有向client返回一个回应的函数都叫做 RedisModule_ReplyWith< something >

向client 返回一个错误
RedisModule_ReplyWithError(RedisModuleCtx * ctx ,const char * err);

  有一个预定义的key的错误 REDISMODULE_ERRORMSG_WRONGTYPE

demo :
RedisModule_ReplyWithError(ctx,"ERR invalid arguments");
返回一个LongLong

  向client返回一个longlongint

RedisModule_ReplyWithLongLong(ctx,12345);
返回一个简单的字符串(段串)

  为了返回一个简单的字符串,这个简单的字符串不能有一个二进制值或者一个长串行(所以这个函数只能返回一个简单的单词,比如”OK”)

RedisModule_ReplyWithSimpleString(ctx,"OK");
返回一个长字符串

  为了返回一个二进制安全的字符串可以用如下俩个接口(所谓二进制安全的字符串意思是它不通过某个特殊字符 (\0) 来判定结尾,比如c++ 的string类型就是一个二进制)

int RedisModule_ReplyWithStringBuffer(RedisModuleCtx *ctx, const char *buf, size_t len);
int RedisModule_ReplyWithString(RedisModuleCtx *ctx, RedisModuleString *str);
回应一个数组信息

  有的时候为了回应一堆信息,我们可以数组的形式reply , 你仅仅需要先用一个函数先发生出去你的数组的长度,然后调用相应数量的上面那些回应的API

RedisModule_ReplyWithArray(ctx,2);
RedisModule_ReplyWithStringBuffer(ctx,"age",3);
RedisModule_ReplyWithLongLong(ctx,22);

  我们回应的数组中的某个元素也可以是一个数组如下

Array 第一个元素是string 第二个元素是一个array
RedisModule_ReplyWithArray(ctx,2);
RedisModule_ReplyWithStringBuffer(ctx,"age",3);
RedisModule_ReplyWithArray(ctx,3);
RedisModule_ReplyWithLongLong(ctx,22);
RedisModule_ReplyWithLongLong(ctx,22);
RedisModule_ReplyWithLongLong(ctx,22);
动态设置Reply 数组

  有时想预先知道一个我们要操作的数组长度是不可能的,列如当我们用Redis module 实现一个FACTOR的命令,它的参数是一个数组结果输出是它的素数因子。为了实现上面的功能,我们可以先通过一个特殊的参数创建数组(RedisModule_ReplyWithArray(ctx, int)),然后在执行完相应的操作后,最后再设置这个数组的长度。

RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);  //设置未知长度的数组
RedisModule_ReplySetArrayLength(ctx, number_of_items); // 最终再设置数组长度

  上面是俩个函数的原型,下面是demo

RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
number_of_factors = 0;
while(still_factors) {
    RedisModule_ReplyWithLongLong(ctx, some_factor);
    number_of_factors++;
}
RedisModule_ReplySetArrayLength(ctx, number_of_factors);

  遍历元素然后按条件过滤一部分再返回结果也是这个特性经常使用到的列子。向前面讲的返回一个内嵌数组也是可以的,对于内嵌数组来讲,SetArrayLength函数只会设置上一个最近调用 ReplywithArray的数组的大小。

RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
... generate 100 elements ...
RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
... generate 10 elements ...
RedisModule_ReplySetArrayLength(ctx, 10);
RedisModule_ReplySetArrayLength(ctx, 100);

低级API

  上面讲完高级API了接了下来我们讲低级API。从上面的介绍可以看到所谓redis_module的高级API就是通过RedisModule_Call 函数直接去可以去执行相应的命令,高级API 执行命令 步骤是下面这样的

  1. RedisModule_Call 调用
  2. RedisModule_ReplyWith … 回应结果

  所以看高级API 更像一个bash,它可以直接通过RedisModule_Call 执行Redis的命令去返回结果,而不是直接通过操作底层数据结构返回相应的结果,所以接下来介绍低级API执行/创建命令的过程。

module 命令的参数检测

  经常有一些命令需要去检测参数的数量和参数的类型到底对不对。当有一个参数个数出错的情况下,我们可以调用下面的 API去返回一个错误信息

int RedisModule_WrongArity(ctx);
demo
if (argc != 2) return RedisModule_WrongArity(ctx);

  检测参数类型包含俩步,第一步打开key ,第二步检测它的类型。注意我们只应该去执行参数和类型都正确的命令,如果不正确不应该再进行后面的API操作。

RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1],
    REDISMODULE_READ|REDISMODULE_WRITE);

int keytype = RedisModule_KeyType(key);
if (keytype != REDISMODULE_KEYTYPE_STRING &&
    keytype != REDISMODULE_KEYTYPE_EMPTY)
{
    RedisModule_CloseKey(key);
    return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE);
}
低级API访问一个key

  低级API允许对 key 执行一些相应的操作在与这个key直接关联的value上,它可以带有与原生Redis命令一样的速度去执行module命令。一旦一个key被打开了,那么就会有一个与之关联的RedisModuleKey的指针返回回来,这个指针可以传给其他低级API去操作这个key。因为低级API是相当的快的,所以它不能在运行时有太多的检测,另外用户必须意识到以下规则

  1. 多次打开一个相同的key,对其进行写操作是未定义行为可能造成崩溃
  2. 当一个key被打开,它是可以通过其他低级API去访问的。举个反面列子,当打开一个key,然后通过高级API去执行一个del 操作将会造成崩溃,但是如果我们通过执行一些其他的低级API在这个key 上然后关闭它,接着再按照 open - dosth - close 这样的顺序操作它是没问题的,所以不要通过低级API去打开一个key 的同时再通过高级API操作它。
打开key的API
RedisModuleKey * RedisModule_OpenKey(RedisModuleCtx * ctx ,RedisModuleString *str ,int 
mode);

demo
RedisModuleKey *key;
key = RedisModule_OpenKey(ctx,argv[1],REDISMODULE_READ);
mode : REDISMODULE_READ 、REDISMODULE_WRITE 、REDISMODULE_READ | REDISMODULE_WRITE

  函数声明大概如上,接下来介绍一些注意点

  1. Write 打开,如果key 不存在就新创建key
  2. Write 打开,也可以对key进行读取(但是不应该依赖这个特性它是Redis内部实现的一些原因)
  3. Read 打开,如果key 不存在返回NULL
关闭key

  我们打开完key后,执行完操作后,应该及时关闭它。如果开启了自动内存管理机制,那么我们就可以不去close了,当这个module函数返回后它会自动的关闭还在打开的key。

RedisModule_CloseKey(key);
Get 一个key类型

猜你喜欢

转载自blog.csdn.net/sdoyuxuan/article/details/82260808