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 函数在所有版本中都不可用。如果 API 函数在当前运行的 Redis 版本中没有实现,则函数指针将被设置为 NULL。这允许模块在使用 API 函数之前检查其是否存在

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 返回的新的字符串,如果这些 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 缓冲区等等。

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

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 -- 长长整型。
  • v -- RedisModuleString 对象数组。
  • ! -- 此修饰符仅告诉函数将命令复制到副本和 AOF。从参数解析的角度来看,它被忽略。
  • A -- 此修饰符在给出 ! 时,指示抑制 AOF 传播:命令将仅传播到副本。
  • R -- 此修饰符在给出 ! 时,指示抑制副本传播:命令将仅在启用时传播到 AOF。

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

当命令名称无效,格式说明符使用未识别的字符,或当命令以错误的参数数量调用时,将返回 NULL。在上述情况下,errno 变量将设置为 EINVAL。当在启用了集群的实例中,目标键与非本地哈希槽有关时,也会返回 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。可以使用 | 将两种模式按位 OR 起来,以在两种模式下打开键。目前,以写入模式打开的键也可以以读取模式访问,但这应该被视为实现细节。在合理的模块中应该使用正确的模式。

您可以以写入模式打开不存在的键,因为在尝试写入键时会创建键。但是,当只为了读取而打开键时,如果键不存在,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,因为该函数只能将过期时间与已打开的键相关联(不存在的打开键仅用于使用特定于数据类型的写操作创建新值)。

同样,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 命令一样使用模块命令,在复制的 Redis 实例中,或者使用 AOF 文件进行持久化,那么模块命令必须以一致的方式处理它们的复制。

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

因此,通常情况下,短暂的分配是池分配器的良好选择。

编写与 Redis 集群兼容的命令

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

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