Redis 模块 API

Redis 模块编写入门

模块文档由以下页面组成

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

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 指针数组传递给模块 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 也用于访问 Redis 中不可用的 API 功能。

一般来说,模块开发者应该优先使用低级 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 -- 以 null 结尾的 C 字符串指针。
  • b -- C 缓冲区,需要两个参数:C 字符串指针和 size_t 长度。
  • s -- RedisModuleString,在 argv 中接收,或由其他返回 RedisModuleString 对象的 Redis 模块 API 返回。
  • l -- 长整型。
  • v -- RedisModuleString 对象数组。
  • ! -- 此修饰符只是告诉函数将命令复制到副本和 AOF。从参数解析的角度来看,它会被忽略。
  • A -- 当给出 ! 时,此修饰符表示抑制 AOF 传播:命令将仅传播到副本。
  • R -- 当给出 ! 时,此修饰符表示抑制副本传播:命令将仅传播到已启用的 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 空回复。

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

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_READ` 或 `REDISMODULE_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)。

管理键过期时间 (TTL)

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

使用一个函数来查询当前打开键的过期时间。

mstime_t RedisModule_GetExpire(RedisModuleKey *key);

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

为了更改键的过期时间,可以使用以下函数。

int RedisModule_SetExpire(RedisModuleKey *key, mstime_t expire);

当对不存在的键调用时,将返回 `REDISMODULE_ERR`,因为该函数只能将过期时间与现有的打开键关联(不存在的打开键仅用于使用特定于数据类型的写入操作创建新值)。

同样,`过期时间` 以毫秒为单位指定。如果键当前没有过期时间,则设置新的过期时间。如果键已经存在过期时间,则用新值替换它。

如果键有过期时间,并且使用特殊值 `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 时,您不应该使用任何其他复制函数,因为它们不能保证混合使用。

然而,这不是唯一的选项。还可以使用类似于 `RedisModule_Call()` 的 API 精确地告诉 Redis 要复制哪些命令作为命令执行的效果,但它不是调用命令,而是将命令发送到 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`、`realloc` 和 `strdup` 的精确替代品。这些函数是

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);
评价此页面