本机类型的模块 API

如何在 Redis 模块中使用原生类型

Redis 模块可以在高级、 通过调用 Redis 命令,在低级别通过作数据结构 径直。

通过使用这些功能在现有 Redis 数据结构,或者使用字符串 DMA 对模块进行编码 数据结构转换为 Redis 字符串中,则可以创建感觉像是在导出新数据类型的模块。但是,对于更复杂的 问题,这还不够,并且要实现新的数据结构 需要 module。

我们将 Redis 模块实现新数据结构的能力称为 感觉就像原生 Redis 原生类型支持一样。本文档介绍 Redis 模块系统导出的 API 以创建新数据 结构并处理 RDB 文件中的序列化,重写过程 在 AOF 中,通过TYPE命令,依此类推。

本机类型概述

导出原生类型的模块由以下主要部分组成:

  • 实现某种新的数据结构和对新数据结构进行作的命令。
  • 一组回调,用于处理:RDB 保存、RDB 加载、AOF 重写、释放与键关联的值、计算要与DEBUG DIGEST命令。
  • 一个 9 个字符的名称,对于每个模块本机数据类型都是唯一的。
  • 编码版本,用于将特定于模块的数据版本持久化到 RDB 文件中,以便模块能够从 RDB 文件加载较旧的表示形式。

虽然处理 RDB 加载、保存和 AOF 重写乍一看可能看起来很复杂,但模块 API 提供了非常高级的功能来处理所有这些,而无需用户处理读/写错误,因此在实践中,为 Redis 编写新的数据结构是一项简单的任务。

一个非常易于理解但完整的本机类型实现示例 在 Redis 发行版的/modules/hellotype.c文件。 鼓励读者通过查看此示例来阅读文档 implementation 来了解事物在实践中的应用方式。

注册新的数据类型

为了在 Redis 核心中注册新的原生类型,该模块需要 来声明一个全局变量,该变量将保存对数据类型的引用。 用于注册数据类型的 API 将返回一个数据类型引用,该引用将 存储在全局变量中。

static RedisModuleType *MyType;
#define MYTYPE_ENCODING_VERSION 0

int RedisModule_OnLoad(RedisModuleCtx *ctx) {
RedisModuleTypeMethods tm = {
    .version = REDISMODULE_TYPE_METHOD_VERSION,
    .rdb_load = MyTypeRDBLoad,
    .rdb_save = MyTypeRDBSave,
    .aof_rewrite = MyTypeAOFRewrite,
    .free = MyTypeFree
};

    MyType = RedisModule_CreateDataType(ctx, "MyType-AZ",
	MYTYPE_ENCODING_VERSION, &tm);
    if (MyType == NULL) return REDISMODULE_ERR;
}

从上面的示例中可以看出,需要一个 API 调用才能 注册新类型。但是,许多函数指针以 参数。某些是可选的,而有些是必需的。上述集合 of 方法,.digest.mem_usage是可选的 并且目前实际上不受模块内部支持,因此对于 现在你可以忽略它们。

ctx参数是我们在OnLoad功能。 类型name是字符集中的 9 个字符的名称,其中包括 从A-Z,a-z,0-9,加上下划线和减号字符。_-

请注意,对于 Redis 中的每种数据类型,此名称必须是唯一的 ecosystem,所以要有创意,如果它使 sense 的 Intent,并尝试使用将类型 name 与 name 混合的约定 创建包含 9 个字符的唯一名称。

注意:名称正好是 9 个字符或 类型的注册将失败。阅读更多内容以了解原因。

例如,如果我正在构建一个 b 树数据结构,我的名字是 antirez,我会将我的类型 btree1-az 命名为 btree1-az。名称,转换为 64 位整数 存储在 RDB 文件中,并将在 加载 RDB 数据是为了解析哪个模块可以加载数据。如果 Redis 找不到匹配的模块,则将整数转换回名称,以便 向用户提供一些线索,说明缺少什么模块才能加载 数据。

类型名称还用作TYPE命令 带有一个保存已注册类型的键。

encverargument 是模块用于存储数据的编码版本 在 RDB 文件中。例如,我可以从编码版本 0 开始, 但是稍后当我发布模块的 2.0 版本时,我可以将编码切换到 更好的东西。新模块将注册为编码版本 1, 因此,当它保存新的 RDB 文件时,新版本将存储在磁盘上。然而 加载 RDB 文件时,模块rdb_load方法将被调用,即使 找到不同编码版本(以及编码版本)的数据 作为参数传递给rdb_load),这样模块仍然可以加载旧的 RDB 文件。

最后一个参数是用于将类型方法传递给 注册功能:rdb_load,rdb_save,aof_rewrite,digestfreemem_usage都是具有以下原型和用途的回调:

typedef void *(*RedisModuleTypeLoadFunc)(RedisModuleIO *rdb, int encver);
typedef void (*RedisModuleTypeSaveFunc)(RedisModuleIO *rdb, void *value);
typedef void (*RedisModuleTypeRewriteFunc)(RedisModuleIO *aof, RedisModuleString *key, void *value);
typedef size_t (*RedisModuleTypeMemUsageFunc)(void *value);
typedef void (*RedisModuleTypeDigestFunc)(RedisModuleDigest *digest, void *value);
typedef void (*RedisModuleTypeFreeFunc)(void *value);
  • rdb_load在从 RDB 文件加载数据时调用。它以与rdb_save生产。
  • rdb_save在将数据保存到 RDB 文件时调用。
  • aof_rewrite在重写 AOF 时调用,并且模块需要告诉 Redis 重新创建给定键内容的命令序列是什么。
  • digestDEBUG DIGEST,并找到包含此模块类型的 key。目前尚未实现,因此函数 ca 留空。
  • mem_usageMEMORYcommand 请求特定键消耗的总内存,并用于获取 module 值使用的字节数。
  • free当具有模块本机类型的键被删除时调用DEL或者以任何其他方式,以便让模块回收与此类值关联的内存。

为什么模块类型需要 9 个字符的名称

当 Redis 持久化到 RDB 文件时,特定于模块的数据类型需要 也被持久化。现在 RDB 文件是键值对序列 如下所示:

[1 byte type] [key] [a type specific value]

1 字节类型标识字符串、列表、集等。在这种情况下 of modules 数据中,它被设置为特殊值module data,但 当然这还不够,我们需要链接特定 值,该值具有能够加载和处理它的特定模块类型。

因此,当我们保存type specific value关于模块,我们在它前面加上 一个 64 位整数。64 位足以存储所需的信息 为了查找可以处理该特定类型的模块,但 足够短,以便我们可以为 RDB 中存储的每个模块值添加前缀 而不会使最终的 RDB 文件太大。同时,该解决方案 的 64 位签名前缀值不需要执行 奇怪的事情,比如在 RDB 头中定义特定于 类型。一切都很简单。

因此,您可以以 64 位存储的内容以识别给定的模块 可靠的方法?好吧,如果您构建一个包含 64 个符号的字符集,则可以 轻松存储 9 个 6 位字符,只剩下 10 位,即 来存储类型的编码版本,以便 相同的类型可以在未来发展并提供不同且更多的 RDB 文件的高效或更新的序列化格式。

所以 每个 module 值之前存储的 64 位前缀如下所示:

6|6|6|6|6|6|6|6|6|10

前 9 个元素是 6 位字符,最后 10 位是 encoding 版本。

当 RDB 文件加载回来时,它会读取 64 位值,屏蔽最终的 10 位,并在 modules types cache 中搜索匹配的模块。 找到匹配的 RDB 文件时,将调用加载 RDB 文件值的方法 以 10 位编码版本作为参数,以便模块知道 要加载的数据布局的哪个版本(如果它可以支持多个版本)。

现在,这一切的有趣之处在于,如果 module 类型 无法解析,因为没有加载的模块具有此签名, 我们可以将 64 位值转换回 9 个字符的名称,然后打印 对用户发送包含模块类型名称的错误!这样她或他 立即意识到出了什么问题。

设置和获取密钥

RedisModule_OnLoad()功能 我们还需要能够将 Redis 键设置为我们的原生类型的值。

这通常发生在将数据写入 key 的命令的上下文中。 原生类型 API 允许设置和获取模块原生数据类型的键, 并测试给定键是否已与特定数据的值相关联 类型。

API 使用普通模块RedisModule_OpenKey()低级密钥访问 接口来处理这个问题。这是设置 native 类型的私有数据结构添加到 Redis key 中:

RedisModuleKey *key = RedisModule_OpenKey(ctx,keyname,REDISMODULE_WRITE);
struct some_private_struct *data = createMyDataStructure();
RedisModule_ModuleTypeSetValue(key,MyType,data);

函数RedisModule_ModuleTypeSetValue()在打开键控符时使用 进行写入,并获取三个参数:键句柄、对 native 类型,如在类型注册期间获取的,最后是void*Pointer,该指针包含实现模块本机类型的私有数据。

请注意,Redis 完全不知道您的数据包含什么。它会 只需按顺序调用您在方法注册期间提供的回调 对类型执行作。

同样,我们可以使用以下函数从 key 中检索私有数据:

struct some_private_struct *data;
data = RedisModule_ModuleTypeGetValue(key);

我们还可以测试一个键,将我们的原生类型作为 value:

if (RedisModule_ModuleTypeGetType(key) == MyType) {
    /* ... do something ... */
}

但是,为了让调用执行正确的作,我们需要检查密钥 为空,如果它包含正确类型的值,依此类推。所以 实现写入本机类型的命令的惯用代码 是这样的:

RedisModuleKey *key = RedisModule_OpenKey(ctx,argv[1],
    REDISMODULE_READ|REDISMODULE_WRITE);
int type = RedisModule_KeyType(key);
if (type != REDISMODULE_KEYTYPE_EMPTY &&
    RedisModule_ModuleTypeGetType(key) != MyType)
{
    return RedisModule_ReplyWithError(ctx,REDISMODULE_ERRORMSG_WRONGTYPE);
}

然后,如果我们成功验证了密钥不是错误的类型,并且 我们要写入它,我们通常想要创建一个新的数据结构 if 键为空,或者检索对与 key (如果已经有一个):

/* Create an empty value object if the key is currently empty. */
struct some_private_struct *data;
if (type == REDISMODULE_KEYTYPE_EMPTY) {
    data = createMyDataStructure();
    RedisModule_ModuleTypeSetValue(key,MyTyke,data);
} else {
    data = RedisModule_ModuleTypeGetValue(key);
}
/* Do something with 'data'... */

免费方法

如前所述,当 Redis 需要释放一个包含原生类型的 key 时 值,它需要模块的帮助才能释放内存。这 是我们传递freecallback 中:

typedef void (*RedisModuleTypeFreeFunc)(void *value);

free 方法的一个小实现可以是这样的, 假设我们的数据结构由单个分配组成:

void MyTypeFreeCallback(void *value) {
    RedisModule_Free(value);
}

然而,在更真实的世界里,人们会调用一些函数来执行 复杂内存回收,通过将 void 指针强制转换为某个结构 并释放构成该值的所有资源。

RDB 加载和保存方法

RDB 保存和加载回调需要创建(并加载回)一个 数据类型在磁盘上的表示形式。Redis 提供高级 API 它可以自动在 RDB 文件中存储以下类型:

  • 无符号 64 位整数。
  • 有符号 64 位整数。
  • 双打。
  • 字符串。

由模块使用上述基础找到可行的表示 类型。但是请注意,虽然 integer 和 double 值是存储的 并以架构和字节序无关的方式加载,如果您使用 原始字符串保存 API,例如,将结构保存到磁盘上,您 必须自己关心这些细节。

以下是执行 RDB 保存和加载的函数列表:

void RedisModule_SaveUnsigned(RedisModuleIO *io, uint64_t value);
uint64_t RedisModule_LoadUnsigned(RedisModuleIO *io);
void RedisModule_SaveSigned(RedisModuleIO *io, int64_t value);
int64_t RedisModule_LoadSigned(RedisModuleIO *io);
void RedisModule_SaveString(RedisModuleIO *io, RedisModuleString *s);
void RedisModule_SaveStringBuffer(RedisModuleIO *io, const char *str, size_t len);
RedisModuleString *RedisModule_LoadString(RedisModuleIO *io);
char *RedisModule_LoadStringBuffer(RedisModuleIO *io, size_t *lenptr);
void RedisModule_SaveDouble(RedisModuleIO *io, double value);
double RedisModule_LoadDouble(RedisModuleIO *io);

这些函数不需要模块进行任何错误检查,可以 始终假设调用成功。

例如,假设我有一个本机类型,它实现了一个 double 值,其结构如下:

struct double_array {
    size_t count;
    double *values;
};

rdb_save方法可能如下所示:

void DoubleArrayRDBSave(RedisModuleIO *io, void *ptr) {
    struct dobule_array *da = ptr;
    RedisModule_SaveUnsigned(io,da->count);
    for (size_t j = 0; j < da->count; j++)
        RedisModule_SaveDouble(io,da->values[j]);
}

我们所做的是存储每个 double 后面的元素数量 价值。因此,稍后我们必须在rdb_load方法中,我们将执行如下作:

void *DoubleArrayRDBLoad(RedisModuleIO *io, int encver) {
    if (encver != DOUBLE_ARRAY_ENC_VER) {
        /* We should actually log an error here, or try to implement
           the ability to load older versions of our data structure. */
        return NULL;
    }

    struct double_array *da;
    da = RedisModule_Alloc(sizeof(*da));
    da->count = RedisModule_LoadUnsigned(io);
    da->values = RedisModule_Alloc(da->count * sizeof(double));
    for (size_t j = 0; j < da->count; j++)
        da->values[j] = RedisModule_LoadDouble(io);
    return da;
}

load 回调只是从数据中重建数据结构 我们存储在 RDB 文件中。

请注意,虽然写入和读取的 API 上没有错误处理 从磁盘上,加载回调仍然可以在错误时返回 NULL,以防 它读取的读数看起来不正确。在这种情况下,Redis 只会 panic。

AOF 重写

void RedisModule_EmitAOF(RedisModuleIO *io, const char *cmdname, const char *fmt, ...);

分配内存

modules 数据类型应尝试使用RedisModule_Alloc()Functions 系列 为了分配、重新分配和释放用于实现本机数据结构的堆内存(有关详细信息,请参阅其他 Redis 模块文档)。

这不仅有助于 Redis 能够考虑模块使用的内存,而且还具有更多优势:

  • Redis 使用jemalloc分配器,这通常可以防止使用 libc 分配器可能导致的碎片问题。
  • 从 RDB 文件加载字符串时,本机类型 API 能够返回直接使用RedisModule_Alloc(),这样模块就可以直接把这个内存链接到数据结构表示中,避免了无用的数据复制。

即使您使用外部库来实现数据结构, module API 提供的 allocation 函数与malloc(),realloc(),free()strdup(),因此将 为了使用这些函数应该是微不足道的。

如果您有一个使用 libc 的外部库malloc(),并且您希望 为避免手动将所有调用替换为 Redis 模块 API 调用, 一种方法是使用简单的宏来替换 libc 调用 使用 Redis API 调用。像这样的事情可能会起作用:

#define malloc RedisModule_Alloc
#define realloc RedisModule_Realloc
#define free RedisModule_Free
#define strdup RedisModule_Strdup

但是请记住,将 libc 调用与 Redis API 调用混合会导致 陷入困境和崩溃,因此如果您使用宏替换调用,则需要 确保所有调用都已正确替换,并且代码中带有 例如,替换的调用永远不会尝试调用RedisModule_Free()使用 libc 分配指针malloc().

为本页评分
返回顶部 ↑