*Redis 模块:API 介绍

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

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

Redis 模块使得使用外部模块扩展 Redis 功能成为可能,以与在核心内部实现相似的速度和功能实现新的 Redis 命令。

Redis 模块是动态库,可以在启动时或使用 MODULE LOAD 命令加载到 Redis 中。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 模块传递配置参数

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

loadmodule mymodule.so foo bar 1234

在上面的示例中,字符串 foobar123 将作为 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) {
    /* 使用 '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 中接收的字符串,它是 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 或返回 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);
    /* 使用 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");

我们在上面的示例中已经看到了如何回复长整型:

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() 的调用,后跟发出子数组元素的调用。

*返回动态长度的数组

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

RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);

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

RedisModule_ReplySetArrayLength(ctx, number_of_items);

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

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

此功能的另一个常见用例是迭代某些集合的数组并只返回通过某种过滤的项。

可以具有多个嵌套的延迟回复数组。每次调用 SetArray() 将设置最近相应调用 ReplyWithArray() 的长度:

RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
... 生成 100 个元素 ...
RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
... 生成 10 个元素 ...
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)。

*管理键过期(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 实例或使用 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 模块特定的函数,它们是 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);