Redis 模块 API

编写Redis模块简介

模块文档由以下页面组成:

  • Redis模块介绍(本文件)。关于Redis模块系统和API的概述。从这里开始阅读是个好主意。
  • 实现原生数据类型 涵盖了将原生数据类型实现到模块中的内容。
  • Blocking operations 展示了如何编写不会立即回复的阻塞命令,这些命令会阻塞客户端,但不会阻塞Redis服务器,并在可能时提供回复。
  • Redis modules API reference 是从 RedisModule 函数的 module.c 顶部注释生成的。它是理解每个函数如何工作的良好参考。

Redis 模块使得可以使用外部模块扩展 Redis 功能,快速实现具有与核心本身相似功能的新 Redis 命令。

Redis 模块是可以在启动时加载到 Redis 中的动态库,或者使用 MODULE LOAD 命令加载。Redis 以单个 C 头文件 redismodule.h 的形式导出了一个 C API。模块通常是用 C 编写的,但也可以使用 C++ 或其他具有 C 绑定功能的语言。

模块被设计成可以加载到不同版本的Redis中,因此一个给定的模块不需要为了与特定版本的Redis一起运行而进行设计或重新编译。出于这个原因,模块将使用特定的API版本注册到Redis核心。当前的API版本是“1”。

本文档是关于Redis模块的alpha版本。API、功能和其他细节在未来可能会有所变化。

加载模块

为了测试您正在开发的模块,您可以使用以下redis.conf配置指令加载模块:

loadmodule /path/to/mymodule.so

也可以在运行时使用以下命令加载模块:

MODULE LOAD /path/to/mymodule.so

为了列出所有加载的模块,请使用:

MODULE LIST

最后,您可以使用以下命令卸载(如果愿意,稍后可以重新加载)模块:

MODULE UNLOAD mymodule

请注意,上面的mymodule不是没有.so后缀的文件名,而是模块用来注册到Redis核心的名称。可以使用MODULE LIST获取该名称。然而,最佳实践是动态库的文件名与模块用来注册到Redis核心的名称相同。

你可以编写的最简单的模块

为了展示模块的不同部分,这里我们将展示一个非常简单的模块,该模块实现了一个输出随机数的命令。

#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, "fast random",
        0, 0, 0) == REDISMODULE_ERR)
        return REDISMODULE_ERR;

    return REDISMODULE_OK;
}

示例模块有两个函数。一个实现了一个名为HELLOWORLD.RAND的命令。这个函数是该模块特有的。然而,另一个名为RedisModule_OnLoad()的函数必须存在于每个Redis模块中。它是模块初始化的入口点,用于注册其命令以及可能使用的其他私有数据结构。

请注意,模块调用命令时,最好使用模块名称后跟一个点,最后是命令名称,例如HELLOWORLD.RAND。这样可以减少冲突的可能性。

请注意,如果不同的模块有冲突的命令,它们将无法在Redis中同时工作,因为函数RedisModule_CreateCommand将在其中一个模块中失败,因此模块加载将中止并返回错误状态。

模块初始化

上面的例子展示了函数 RedisModule_Init() 的用法。 它应该是模块 OnLoad 函数调用的第一个函数。 以下是函数原型:

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

Init 函数向 Redis 核心宣布模块具有给定的名称,其版本(由 MODULE LIST 报告),并且愿意使用特定版本的 API。

如果API版本错误,名称已被占用,或者存在其他类似错误,函数将返回REDISMODULE_ERR,模块的OnLoad函数应尽快返回错误。

在调用Init函数之前,不能调用其他API函数,否则模块将出现段错误,Redis实例将崩溃。

第二个调用的函数,RedisModule_CreateCommand,用于向Redis核心注册命令。以下是原型:

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

如你所见,大多数Redis模块API调用都将模块的context作为第一个参数,以便它们能够引用调用它的模块、执行给定命令的命令和客户端等。

要创建一个新命令,上述函数需要上下文、命令的名称、指向实现命令的函数的指针、命令的标志以及命令参数中键名的位置。

实现命令的函数必须具有以下原型:

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

命令函数的参数只是上下文,它将传递给所有其他API调用,命令参数向量,以及用户传递的参数总数。

如你所见,参数是以指向特定数据类型的指针形式提供的,即RedisModuleString。这是一种不透明的数据类型,你可以通过API函数来访问和使用它,直接访问其字段是不必要的。

放大查看示例命令的实现,我们可以找到另一个调用:

int RedisModule_ReplyWithLongLong(RedisModuleCtx *ctx, long long integer);

此函数向调用命令的客户端返回一个整数,就像其他Redis命令一样,例如INCRSCARD

模块清理

在大多数情况下,不需要特殊的清理工作。 当一个模块被卸载时,Redis会自动注销命令并取消订阅通知。 然而,如果一个模块包含一些持久内存或配置,模块可能包含一个可选的RedisModule_OnUnload函数。 如果一个模块提供了这个函数,它将在模块卸载过程中被调用。 以下是函数原型:

int RedisModule_OnUnload(RedisModuleCtx *ctx);

OnUnload 函数可以通过返回 REDISMODULE_ERR 来阻止模块卸载。 否则,应返回 REDISMODULE_OK

Redis模块的设置和依赖

Redis模块不依赖于Redis或其他库,也不需要与特定的redismodule.h文件一起编译。为了创建一个新模块,只需将最新版本的redismodule.h复制到您的源代码树中,链接所有您想要的库,并创建一个动态库,该库导出RedisModule_OnLoad()函数符号。

该模块将能够加载到不同版本的Redis中。

可以设计一个模块来支持新旧版本的Redis,其中某些API函数在所有版本中不可用。 如果在当前运行的Redis版本中未实现API函数,则函数指针设置为NULL。 这允许模块在使用函数之前检查它是否存在:

if (RedisModule_SetCommandInfo != NULL) {
    RedisModule_SetCommandInfo(cmd, &info);
}

redismodule.h的最新版本中,定义了一个便利宏RMAPI_FUNC_SUPPORTED(funcname)。 使用宏或仅与NULL比较是个人偏好的问题。

将配置参数传递给Redis模块

当模块通过MODULE LOAD命令加载,或者使用redis.conf文件中的loadmodule指令加载时,用户可以在模块文件名后添加参数来传递配置参数给模块:

loadmodule mymodule.so foo bar 1234

在上面的例子中,字符串 foobar1234 将作为 RedisModuleString 指针数组传递给模块的 OnLoad() 函数的 argv 参数。传递的参数数量存储在 argc 中。

本文档的其余部分将解释如何访问这些字符串。通常,模块会将模块配置参数存储在某个static全局变量中,以便在整个模块范围内访问,从而使配置可以改变不同命令的行为。

使用 RedisModuleString 对象

传递给模块命令的命令参数向量 argv,以及其他模块API函数的返回值,都是 RedisModuleString 类型。

通常你直接将模块字符串传递给其他API调用,然而有时你可能需要直接访问字符串对象。

有一些函数用于处理字符串对象:

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

上述函数通过返回其指针并在len中设置其长度来访问字符串。 你永远不应该写入字符串对象指针,正如你可以从const指针限定符中看到的那样。

然而,如果你愿意,你可以使用以下API创建新的字符串对象:

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

上述命令返回的字符串必须使用相应的RedisModule_FreeString()调用来释放:

void RedisModule_FreeString(RedisModuleString *str);

然而,如果你想避免手动释放字符串,本文档后面介绍的自动内存管理可以是一个很好的替代方案,它会为你完成这项工作。

请注意,通过参数向量 argv 提供的字符串永远不需要释放。您只需要释放您创建的新字符串,或者其他API返回的新字符串,其中明确指出返回的字符串必须被释放。

从数字创建字符串或将字符串解析为数字

从整数创建新字符串是一个非常常见的操作,因此有一个函数可以做到这一点:

RedisModuleString *mystr = RedisModule_CreateStringFromLongLong(ctx,10);

同样地,为了将字符串解析为数字:

long long myval;
if (RedisModule_StringToLongLong(ctx,argv[1],&myval) == REDISMODULE_OK) {
    /* Do something with 'myval' */
}

从模块访问Redis键

大多数Redis模块,为了有用,必须与Redis数据空间进行交互(这并不总是正确的,例如一个ID生成器可能永远不会触及Redis键)。Redis模块有两个不同的API来访问Redis数据空间,一个是低级API,提供非常快速的访问和一组操作Redis数据结构的函数。另一个API更高级,允许调用Redis命令并获取结果,类似于Lua脚本访问Redis的方式。

高级API对于访问那些不作为API提供的Redis功能也非常有用。

一般来说,模块开发者应该优先选择低级API,因为使用低级API实现的命令运行速度与原生Redis命令相当。然而,高级API确实有它的使用场景。例如,通常瓶颈可能是处理数据而不是访问数据。

还要注意,有时使用低级API并不比使用高级API更难。

调用Redis命令

访问Redis的高级API是RedisModule_Call()函数的总和,以及访问Call()返回的回复对象所需的函数。

RedisModule_Call 使用一种特殊的调用约定,带有格式说明符,用于指定您作为参数传递给函数的对象类型。

Redis命令仅通过命令名称和参数列表调用。 然而,在调用命令时,参数可能来自不同类型的字符串:以null结尾的C字符串,从命令实现中的argv参数接收的RedisModuleString对象,带有指针和长度的二进制安全C缓冲区,等等。

例如,如果我想使用第一个参数(键)调用INCRBY,该参数是从参数向量argv中接收的字符串,argv是一个RedisModuleString对象指针的数组,并且第二个参数(增量)是一个表示数字“10”的C字符串,我将使用以下函数调用:

RedisModuleCallReply *reply;
reply = RedisModule_Call(ctx,"INCRBY","sc",argv[1],"10");

第一个参数是上下文,第二个参数始终是一个以null结尾的C字符串,表示命令名称。第三个参数是格式说明符,其中每个字符对应于后续参数的类型。在上述情况下,"sc"表示一个RedisModuleString对象和一个以null结尾的C字符串。其他参数只是指定的两个参数。实际上,argv[1]是一个RedisModuleString,而"10"是一个以null结尾的C字符串。

这是格式说明符的完整列表:

  • c -- 以空字符结尾的C字符串指针。
  • b -- C 缓冲区,需要两个参数:C 字符串指针和 size_t 长度。
  • s -- 作为 argv 接收的 RedisModuleString 或由其他返回 RedisModuleString 对象的 Redis 模块 API 返回。
  • l -- 长整型整数。
  • v -- RedisModuleString 对象的数组。
  • ! -- 这个修饰符只是告诉函数将命令复制到副本和AOF。从参数解析的角度来看,它被忽略了。
  • A -- 当给定!时,此修饰符告诉要抑制AOF传播:命令将仅传播到副本。
  • R -- 当给定!时,此修饰符告诉要抑制副本传播:如果启用了AOF,命令将仅传播到AOF。

该函数在成功时返回一个RedisModuleCallReply对象,在错误时返回NULL。

当命令名称无效、格式说明符使用了不被识别的字符,或者命令调用时参数数量错误时,将返回NULL。在上述情况下,errno变量被设置为EINVAL。在启用了集群的实例中,当目标键位于非本地哈希槽时,也会返回NULL。在这种情况下,errno被设置为EPERM

使用 RedisModuleCallReply 对象。

RedisModuleCall 返回的回复对象可以使用 RedisModule_CallReply* 系列函数进行访问。

为了获取类型或回复(对应于Redis协议支持的其中一种数据类型),使用函数RedisModule_CallReplyType()

reply = RedisModule_Call(ctx,"INCRBY","sc",argv[1],"10");
if (RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_INTEGER) {
    long long myval = RedisModule_CallReplyInteger(reply);
    /* Do something with myval. */
}

有效的回复类型有:

  • REDISMODULE_REPLY_STRING 批量字符串或状态回复。
  • REDISMODULE_REPLY_ERROR 错误。
  • REDISMODULE_REPLY_INTEGER 有符号64位整数。
  • REDISMODULE_REPLY_ARRAY 回复数组。
  • REDISMODULE_REPLY_NULL NULL 回复。

字符串、错误和数组都有一个相关的长度。对于字符串和错误,长度对应于字符串的长度。对于数组,长度是元素的数量。要获取回复长度,使用以下函数:

size_t reply_len = RedisModule_CallReplyLength(reply);

为了获取整数回复的值,使用以下函数,如上例所示:

long long reply_integer_val = RedisModule_CallReplyInteger(reply);

使用错误类型的回复对象调用时,上述函数总是返回LLONG_MIN

数组回复的子元素通过以下方式访问:

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

如果您尝试访问超出范围的元素,上述函数将返回NULL。

字符串和错误(它们类似于字符串但类型不同)可以通过以下方式访问,确保永远不要写入结果指针(它作为const指针返回,因此误用必须非常明确):

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

如果回复类型不是字符串或错误,则返回NULL。

RedisCallReply 对象与模块字符串对象(RedisModuleString 类型)不同。然而,有时你可能需要将字符串或整数类型的回复传递给期望模块字符串的 API 函数。

在这种情况下,您可能希望评估使用低级API是否是实现命令的更简单方法,或者您可以使用以下函数从字符串、错误或整数类型的调用回复中创建一个新的字符串对象:

RedisModuleString *mystr = RedisModule_CreateStringFromCallReply(myreply);

如果回复的类型不正确,则返回NULL。 返回的字符串对象应像通常一样使用RedisModule_FreeString()释放, 或通过启用自动内存管理(参见相应部分)来释放。

释放调用回复对象

回复对象必须使用RedisModule_FreeCallReply来释放。对于数组,你只需要释放顶层的回复,而不是嵌套的回复。目前模块实现提供了一个保护措施,以避免在错误释放嵌套回复对象时崩溃,然而这个特性并不保证会一直存在,因此不应被视为API的一部分。

如果您使用自动内存管理(本文档后面会解释),您不需要释放回复(但如果您希望尽快释放内存,仍然可以这样做)。

从Redis命令返回值

与普通的Redis命令一样,通过模块实现的新命令必须能够向调用者返回值。API为此目标导出了一系列函数,以返回Redis协议的常见类型,以及这些类型作为元素的数组。还可以返回带有任何错误字符串和代码的错误(错误代码是错误消息中的初始大写字母,如“BUSY the sever is busy”错误消息中的“BUSY”字符串)。

所有用于向客户端发送回复的函数都称为 RedisModule_ReplyWith

要返回错误,请使用:

RedisModule_ReplyWithError(RedisModuleCtx *ctx, const char *err);

有一个预定义的错误字符串用于键类型错误的错误:

REDISMODULE_ERRORMSG_WRONGTYPE

示例用法:

RedisModule_ReplyWithError(ctx,"ERR invalid arguments");

我们已经在上述示例中看到了如何使用long long进行回复:

RedisModule_ReplyWithLongLong(ctx,12345);

要回复一个简单的字符串,该字符串不能包含二进制值或换行符, (因此适合发送小单词,如“OK”)我们使用:

RedisModule_ReplyWithSimpleString(ctx,"OK");

可以使用两种不同的函数来回复二进制安全的“批量字符串”:

int RedisModule_ReplyWithStringBuffer(RedisModuleCtx *ctx, const char *buf, size_t len);

int RedisModule_ReplyWithString(RedisModuleCtx *ctx, RedisModuleString *str);

第一个函数获取一个C指针和长度。第二个函数获取一个RedisModuleString对象。根据你手头的源类型使用其中一个。

为了返回一个数组,你只需要使用一个函数来发出数组的长度,然后根据数组元素的数量调用上述函数:

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

返回嵌套数组很容易,你的嵌套数组元素只需再次调用RedisModule_ReplyWithArray(),然后调用以发出子数组元素。

返回具有动态长度的数组

有时无法预先知道数组的项目数量。例如,考虑一个实现FACTOR命令的Redis模块,该命令给定一个数字并输出其质因数。与其将数字分解为质因数,将质因数存储到数组中,然后再生成命令回复,更好的解决方案是启动一个长度未知的数组回复,并在稍后设置它。这是通过RedisModule_ReplyWithArray()的一个特殊参数来实现的:

RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_LEN);

上述调用启动了一个数组回复,因此我们可以使用其他ReplyWith调用来生成数组项。最后,为了设置长度,请使用以下调用:

RedisModule_ReplySetArrayLength(ctx, number_of_items);

在FACTOR命令的情况下,这转化为类似于以下代码:

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

此功能的另一个常见用例是遍历某些集合的数组,并仅返回通过某种过滤的数组。

可以拥有多个嵌套数组并延迟回复。 每次调用SetArray()都会设置最近一次对应的 ReplyWithArray()调用的长度:

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

这将创建一个包含100个项目的数组,最后一个元素是一个包含10个项目的数组。

参数数量和类型检查

通常命令需要检查参数的数量和键的类型是否正确。为了报告错误的参数数量,有一个特定的函数叫做RedisModule_WrongArity()。使用方法非常简单:

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

检查错误类型涉及打开密钥并检查类型:

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);
}

请注意,如果键是预期类型,或者如果它是空的,您通常希望继续执行命令。

低级访问键

对键的低级别访问允许直接对与键关联的值对象执行操作,其速度类似于Redis内部用于实现内置命令的速度。

一旦一个键被打开,将返回一个键指针,该指针将与所有其他低级API调用一起使用,以便对键或其关联值执行操作。

因为API旨在非常快速,它不能进行太多的运行时检查,所以用户必须注意遵循某些规则:

  • 多次打开同一个键,其中至少有一个实例是用于写入的,这是未定义的,并可能导致崩溃。
  • 当一个键处于打开状态时,应仅通过低级键API访问。例如,打开一个键,然后使用RedisModule_Call() API对同一键调用DEL将导致崩溃。然而,打开一个键,使用低级API执行一些操作,关闭它,然后使用其他API管理同一键,之后再打开它进行更多操作是安全的。

为了打开一个键,使用了RedisModule_OpenKey函数。它返回一个键指针,我们将使用该指针与所有后续调用来访问和修改值:

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

第二个参数是键名,必须是一个RedisModuleString对象。 第三个参数是模式:REDISMODULE_READREDISMODULE_WRITE。 可以使用|对这两种模式进行按位或操作,以在两种模式下打开键。 目前,以写模式打开的键也可以进行读取,但这应被视为实现细节。在合理的模块中应使用正确的模式。

您可以打开不存在的键进行写入,因为当尝试写入键时,键将被创建。然而,当仅为了读取而打开键时,如果键不存在,RedisModule_OpenKey 将返回 NULL。

一旦你使用完一个密钥,你可以通过以下方式关闭它:

RedisModule_CloseKey(key);

请注意,如果启用了自动内存管理,您不需要手动关闭键。当模块函数返回时,Redis 将负责关闭所有仍然打开的键。

获取键类型

为了获取键的值,请使用RedisModule_KeyType()函数:

int keytype = RedisModule_KeyType(key);

它返回以下值之一:

REDISMODULE_KEYTYPE_EMPTY
REDISMODULE_KEYTYPE_STRING
REDISMODULE_KEYTYPE_LIST
REDISMODULE_KEYTYPE_HASH
REDISMODULE_KEYTYPE_SET
REDISMODULE_KEYTYPE_ZSET

以上只是常见的Redis键类型,增加了一个空类型,表示键指针与一个尚不存在的空键相关联。

创建新密钥

要创建一个新密钥,请打开它以进行写入,然后使用其中一个密钥写入函数进行写入。示例:

RedisModuleKey *key;
key = RedisModule_OpenKey(ctx,argv[1],REDISMODULE_WRITE);
if (RedisModule_KeyType(key) == REDISMODULE_KEYTYPE_EMPTY) {
    RedisModule_StringSet(key,argv[2]);
}

删除键

只需使用:

RedisModule_DeleteKey(key);

如果键未打开以供写入,函数将返回REDISMODULE_ERR。 请注意,键被删除后,它会被设置为可以被新的键命令所针对。 例如,RedisModule_KeyType()将返回它是一个空键,写入它将创建一个新键,可能是另一种类型(取决于使用的API)。

管理键的过期时间(TTLs)

为了控制键的过期,提供了两个函数,能够设置、修改、获取和取消与键关联的生存时间。

一个函数用于查询一个开放密钥的当前过期时间:

mstime_t RedisModule_GetExpire(RedisModuleKey *key);

该函数返回键的生存时间,单位为毫秒,或者返回特殊值REDISMODULE_NO_EXPIRE,表示该键没有关联的过期时间或根本不存在(您可以通过检查键类型是否为REDISMODULE_KEYTYPE_EMPTY来区分这两种情况)。

为了更改键的过期时间,使用以下函数代替:

int RedisModule_SetExpire(RedisModuleKey *key, mstime_t expire);

当调用一个不存在的键时,返回REDISMODULE_ERR,因为该函数只能将过期时间关联到已存在的打开键(不存在的打开键仅用于通过数据类型特定的写操作创建新值)。

再次,expire时间以毫秒为单位指定。如果键当前没有过期时间,则设置一个新的过期时间。如果键已经有过期时间,则用新值替换它。

如果键有过期时间,并且使用特殊值REDISMODULE_NO_EXPIRE作为新的过期时间,则过期时间将被移除,类似于Redis的PERSIST命令。如果键已经是持久的,则不执行任何操作。

获取值的长度

有一个单一的函数用于检索与打开键关联的值的长度。返回的长度是特定于值的,对于字符串来说是字符串长度,对于聚合数据类型来说是元素的数量(列表、集合、排序集合、哈希中有多少个元素)。

size_t len = RedisModule_ValueLength(key);

如果键不存在,函数将返回0:

字符串类型 API

设置一个新的字符串值,就像Redis的SET命令所做的那样,是通过以下方式执行的:

int RedisModule_StringSet(RedisModuleKey *key, RedisModuleString *str);

该函数的工作方式与 Redis 的 SET 命令完全相同,也就是说,如果存在先前的值(任何类型),它将被删除。

访问现有的字符串值是为了速度而使用DMA(直接内存访问)执行的。API将返回一个指针和一个长度,这样就可以访问并在需要时直接修改字符串。

size_t len, j;
char *myptr = RedisModule_StringDMA(key,&len,REDISMODULE_WRITE);
for (j = 0; j < len; j++) myptr[j] = 'A';

在上面的例子中,我们直接在字符串上写入。请注意,如果你想写入,必须确保请求WRITE模式。

DMA指针只有在DMA调用后,使用指针之前没有对密钥执行其他操作时才有效。

有时当我们想要直接操作字符串时,我们也需要改变它们的大小。为此,使用RedisModule_StringTruncate函数。示例:

RedisModule_StringTruncate(mykey,1024);

该函数根据需要截断或扩展字符串,如果之前的长度小于我们请求的新长度,则用零字节填充。如果字符串不存在,因为key与一个空的键相关联,则会创建一个字符串值并与该键关联。

请注意,每次调用StringTruncate()时,我们需要重新获取DMA指针,因为旧的指针可能已失效。

列表类型 API

可以从列表值中推送和弹出值:

int RedisModule_ListPush(RedisModuleKey *key, int where, RedisModuleString *ele);
RedisModuleString *RedisModule_ListPop(RedisModuleKey *key, int where);

在这两个API中,where参数指定是从尾部还是头部进行推送或弹出,使用以下宏:

REDISMODULE_LIST_HEAD
REDISMODULE_LIST_TAIL

RedisModule_ListPop()返回的元素类似于使用RedisModule_CreateString()创建的字符串,它们必须通过RedisModule_FreeString()释放或通过启用自动内存管理来释放。

设置类型 API

正在进行中。

有序集合类型 API

文档缺失,请参考module.c中的顶部注释以了解以下函数:

  • RedisModule_ZsetAdd
  • RedisModule_ZsetIncrby
  • RedisModule_ZsetScore
  • RedisModule_ZsetRem

对于排序集合迭代器:

  • RedisModule_ZsetRangeStop
  • RedisModule_ZsetFirstInScoreRange
  • RedisModule_ZsetLastInScoreRange
  • RedisModule_ZsetFirstInLexRange
  • RedisModule_ZsetLastInLexRange
  • RedisModule_ZsetRangeCurrentElement
  • RedisModule_ZsetRangeNext
  • RedisModule_ZsetRangePrev
  • RedisModule_ZsetRangeEndReached

哈希类型 API

文档缺失,请参考module.c中的顶部注释以了解以下函数:

  • RedisModule_HashSet
  • RedisModule_HashGet

迭代聚合值

正在进行中。

复制命令

如果你想在复制的Redis实例环境中,或者使用AOF文件进行持久化时,像使用普通的Redis命令一样使用模块命令,那么模块命令以一致的方式处理它们的复制是非常重要的。

当使用更高级别的API调用命令时,如果你在RedisModule_Call()的格式字符串中使用"!"修饰符,复制会自动发生,如下例所示:

reply = RedisModule_Call(ctx,"INCRBY","!sc",argv[1],"10");

正如你所看到的格式说明符是"!sc"。感叹号不被解析为格式说明符,但它在内部将命令标记为“必须复制”。

如果你使用上述编程风格,没有问题。 然而,有时事情比这更复杂,你使用低级 API。在这种情况下,如果命令执行中没有副作用,并且 它始终如一地执行相同的工作,那么可以做的事情是 逐字复制用户执行的命令。要做到这一点,你只需要 调用以下函数:

RedisModule_ReplicateVerbatim(ctx);

当你使用上述API时,你不应该使用任何其他复制功能,因为它们不能保证很好地混合使用。

然而,这并不是唯一的选择。也可以精确地告诉 Redis 要复制哪些命令作为命令执行的效果,使用类似于 RedisModule_Call() 的 API,但不是调用命令,而是将其发送到 AOF / 副本流。例如:

RedisModule_Replicate(ctx,"INCRBY","cl","foo",my_increment);

可以多次调用RedisModule_Replicate,每次调用都会发出一个命令。所有发出的序列都被包裹在一个MULTI/EXEC事务中,因此AOF和复制的效果与执行单个命令相同。

请注意,Call() 复制和 Replicate() 复制有一个规则,以防您想要混合使用这两种复制形式(如果有更简单的方法,这不一定是个好主意)。使用 Call() 复制的命令总是在最终的 MULTI/EXEC 块中首先发出,而所有使用 Replicate() 发出的命令将紧随其后。

自动内存管理

通常在用C语言编写程序时,程序员需要手动管理内存。这就是为什么Redis模块API提供了释放字符串、关闭打开的键、释放回复等函数的原因。

然而,考虑到命令是在一个受控环境中执行的,并且使用一组严格的API,Redis能够为模块提供自动内存管理,尽管这可能会牺牲一些性能(大多数情况下,成本非常低)。

当启用自动内存管理时:

  1. 您不需要关闭打开的密钥。
  2. 你不需要释放回复。
  3. 您不需要释放RedisModuleString对象。

然而,如果你愿意,你仍然可以这样做。例如,自动内存管理可能是激活的,但在一个分配大量字符串的循环内部,你可能仍然希望释放不再使用的字符串。

为了启用自动内存管理,只需在命令实现的开始处调用以下函数:

RedisModule_AutoMemory(ctx);

自动内存管理通常是首选方法,然而有经验的C程序员可能不会使用它,以获得一些速度和内存使用上的优势。

将内存分配到模块中

普通的C程序使用malloc()free()来动态分配和释放内存。而在Redis模块中,虽然技术上不禁止使用malloc,但最好使用Redis模块特定的函数,这些函数是mallocfreereallocstrdup的精确替代品。这些函数是:

void *RedisModule_Alloc(size_t bytes);
void* RedisModule_Realloc(void *ptr, size_t bytes);
void RedisModule_Free(void *ptr);
void RedisModule_Calloc(size_t nmemb, size_t size);
char *RedisModule_Strdup(const char *str);

它们的工作方式与libc中的等效调用完全相同,但它们使用Redis使用的相同分配器,并且使用这些函数分配的内存会在INFO命令的内存部分中报告,在强制执行maxmemory策略时也会被计入,并且通常是Redis可执行文件的主要部分。相反,使用libc malloc()在模块内分配的方法对Redis是透明的。

使用模块函数来分配内存的另一个原因是,当在模块内部创建原生数据类型时,RDB加载函数可以直接将反序列化的字符串(来自RDB文件)作为RedisModule_Alloc()分配返回,因此它们可以直接用于加载后填充数据结构,而不必将其复制到数据结构中。

池分配器

有时在命令实现中,需要执行许多小的分配,这些分配在命令执行结束时不会被保留,而只是为了执行命令本身的功能。

这项工作可以更轻松地使用Redis池分配器完成:

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

它的工作方式类似于malloc(),并返回对齐到大于或等于bytes的下一个二次幂的内存(最大对齐为8字节)。然而,它以块的形式分配内存,因此分配的开销很小,更重要的是,当命令返回时,分配的内存会自动释放。

因此,一般来说,短期存活的内存分配是池分配器的良好候选。

编写与Redis集群兼容的命令

文档缺失,请检查module.c中的以下函数:

RedisModule_IsKeysPositionRequest(ctx);
RedisModule_KeyAtPos(ctx,pos);
RATE THIS PAGE
Back to top ↑