Redis 模块 API

Redis 模块编写入门

模块文档包含以下页面

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

Redis 模块使得使用外部模块扩展 Redis 功能成为可能,可以快速实现新的 Redis 命令,其功能类似于核心本身可以完成的功能。

Redis 模块是动态库,可以在启动时加载到 Redis 中,或者使用 MODULE LOAD 命令加载。Redis 导出了一组 C API,形式为单个 C 头文件 redismodule.h。模块主要用 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 指针数组通过 argv 参数传递给模块的 OnLoad() 函数。传递的参数数量在 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 数据空间,一种是提供非常快速访问和一组操作 Redis 数据结构的低级 API。另一种是更高级的 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 中接收到的字符串(一个 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 -- 以 null 结尾的 C 字符串指针。
  • b -- C 缓冲区,需要两个参数:C 字符串指针和 size_t 长度。
  • s -- 从 argv 或返回 RedisModuleString 对象的其他 Redis 模块 API 中接收到的 RedisModuleString。
  • l -- Long long 整型。
  • v -- RedisModuleString 对象数组。
  • ! -- 此修饰符仅告知函数将命令复制到副本和 AOF。从参数解析的角度来看,它被忽略。
  • A -- 此修饰符,当给定 ! 时,表示抑制 AOF 传播:命令仅会传播到副本。
  • R -- 此修饰符,当给定 ! 时,表示抑制副本传播:如果启用了 AOF,命令仅会传播到 AOF。

函数成功时返回 RedisModuleCallReply 对象,失败时返回 NULL。

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

使用 RedisModuleCallReply 对象。

RedisModule_Call 返回可以使用 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<something>

要返回错误,使用

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

可以向列表值中推入(push)和弹出(pop)值

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 模块特定的函数,它们是 malloc, free, reallocstrdup 的精确替代品。这些函数是:

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 的下一个 2 的幂的内存(最大对齐为 8 字节)。但是它以块为单位分配内存,因此分配的开销很小,更重要的是,分配的内存在命令返回时会自动释放。

因此,通常生命周期短暂的分配是内存池分配器的良好候选者。

编写与 Redis 集群兼容的命令

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

RedisModule_IsKeysPositionRequest(ctx);
RedisModule_KeyAtPos(ctx,pos);
评价此页面
返回顶部 ↑