如何使用 C++ 开发 Redis 模块

2023-12-19

在本文中,我将总结 Tair 在使用 C++ 开发 Redis 模块时遇到的一些问题,并将其提炼为最佳实践。目的是为 Redis 模块的用户和开发人员提供帮助。其中一些最佳实践也可以应用于 C 编程语言和其他编程语言。

介绍

从 Redis 5.0 开始,支持模块插件来增强 Redis 的能力。这些插件允许开发新的数据结构,实现命令侦听和过滤,以及扩展新的网络服务。可以肯定地说,模块的引入大大提高了 Redis 的灵活性,降低了 Redis 开发的复杂性。

Redis社区中涌现出众多模块,覆盖各个领域,丰富了生态。这些模块中的大多数都是使用 C 编程语言开发的。但是,Redis 模块也支持使用 C++ 和 Rust 等其他语言进行开发。本文旨在总结 Tair 在使用 C++ 开发 Redis 模块时遇到的问题,并将其作为最佳实践进行介绍。其目的是为 Redis 模块的用户和开发人员提供帮助,其中一些最佳实践也适用于 C 和其他语言。

Redis 模块的工作原理

Redis内核是用C语言开发的,自然而然地就引出了在C编程语言环境下开发插件时要考虑动态链接库。虽然 Redis 确实使用动态链接库,但有几个关键点需要注意:

  1. Redis 内核公开并导出各种 API 供模块使用。这些 API 包括 Redis 核心数据库结构的内存分配接口和操作接口。请务必了解,这些 API 是由 Redis 本身解析和绑定的,而不是由动态连接器解析和绑定的。
  2. Redis 内核使用 dlopen 显式加载模块,而不是依赖于动态链接器的隐式加载。这意味着当模块需要实现特定接口时,Redis 会自动调用模块的入口函数来初始化 API、注册数据结构以及执行其他必要的功能。

装载

Redis内核中模块加载的逻辑如下:

int moduleLoad(const char *path, void **module_argv, int module_argc, int is_loadex) {
    int (*onload)(void *, void **, int);
    void *handle;

    struct stat st;
    if (stat(path, &st) == 0) {
        /* This check is best effort */
        if (!(st.st_mode & (S_IXUSR  | S_IXGRP | S_IXOTH))) {
            serverLog(LL_WARNING, "Module %s failed to load: It does not have execute permissions.", path);
            return C_ERR;
        }
    }

    // Open the module so.
    handle = dlopen(path,RTLD_NOW|RTLD_LOCAL);
    if (handle == NULL) {
        serverLog(LL_WARNING, "Module %s failed to load: %s", path, dlerror());
        return C_ERR;
    }

// Obtain the symbolic address of the onload function in the module.
onload = (int (*)(void *, void **, int))(unsigned long) dlsym(handle,"RedisModule_OnLoad");
    if (onload == NULL) {
        dlclose(handle);
        serverLog(LL_WARNING,
            "Module %s does not export RedisModule_OnLoad() "
            "symbol. Module not loaded.",path);
        return C_ERR;
    }
    RedisModuleCtx ctx;
    moduleCreateContext(&ctx, NULL, REDISMODULE_CTX_TEMP_CLIENT); /* We pass NULL since we don't have a module yet. */
    // Call onload to initialize the module.
    if (onload((void*)&ctx,module_argv,module_argc) == REDISMODULE_ERR) {
        serverLog(LL_WARNING,
            "Module %s initialization failed. Module not loaded",path);
        if (ctx.module) {
            moduleUnregisterCommands(ctx.module);
            moduleUnregisterSharedAPI(ctx.module);
            moduleUnregisterUsedAPI(ctx.module);
            moduleRemoveConfigs(ctx.module);
            moduleFreeModuleStructure(ctx.module);
        }
        moduleFreeContext(&ctx);
        dlclose(handle);
        return C_ERR;
    }

    /* Redis module loaded! Register it. */

    //... irrelevant code is omitted ...

    moduleFreeContext(&ctx);
    return C_OK;
}

API binding

In the initialization function of the module, RedisModule_Init should be called explicitly to initialize the APIs exported by the Redis kernel. Example:

int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
    if (RedisModule_Init(ctx, "helloworld", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) 
      return REDISMODULE_ERR;

    // ... irrelevant code is omitted ...
}

RedisModule_Init 是 redismodule.h 中定义的一个函数,用于导出和绑定 Redis 内核公开的每个 API。

static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) {
    void *getapifuncptr = ((void**)ctx)[0];
    RedisModule_GetApi = (int (*)(const char *, void *)) (unsigned long)getapifuncptr;

    // Bind the APIs exported by Redis.
    REDISMODULE_GET_API(Alloc);
    REDISMODULE_GET_API(TryAlloc);
    REDISMODULE_GET_API(Calloc);
    REDISMODULE_GET_API(Free);
    REDISMODULE_GET_API(Realloc);
    REDISMODULE_GET_API(Strdup);
    REDISMODULE_GET_API(CreateCommand);
    REDISMODULE_GET_API(GetCommand);
  
    // ... irrelevant code is omitted ...
}

让我们先看看REDISMODULE_GET_API在做什么。它是一个宏定义,实质上调用RedisModule_GetApi函数:

#define REDISMODULE_GET_API(name) \
RedisModule_GetApi("RedisModule_" #name, ((void **)&RedisModule_ ## name))

RedisModule_GetApi看起来像是 Redis 内部公开的 API,但我们现在正在执行 API 绑定。绑定前如何获取RedisModule_GetApi函数的地址?答案是,当 Redis 内核调用模块的 OnLoad 函数时,它会通过 RedisModuleCtx 传递 RedisModule_GetApi 函数的地址。您可以在上面看到用于加载模块的代码。在调用 Onload 函数之前,Redis 使用 moduleCreateContext 初始化 RedisModuleCtx,并将其传递给模块。

在 moduleCreateContext 中,Redis 中定义的 RM_GetApi 函数的地址分配给 RedisModuleCtx 的 getapifuncptr 成员。

void moduleCreateContext(RedisModuleCtx *out_ctx, RedisModule *module, int ctx_flags) {
    memset(out_ctx, 0 ,sizeof(RedisModuleCtx));
    // Pass the GetApi address to the module.
    out_ctx->getapifuncptr = (void*)(unsigned long)&RM_GetApi;
    out_ctx->module = module;
    out_ctx->flags = ctx_flags;

    // ... irrelevant code is omitted ...
}

因此,我们可以使用 RedisModuleCtx 来获取模块中的 GetApi 函数。为什么我们用这么一个“奇怪”的方法,((void**)ctx)[0],而不是直接用ctx->getapifuncptr?原因是 RedisModuleCtx 是 Redis 内核中定义的数据结构,其内部结构对模块(不透明指针)不可见。因此,我们可以利用 getapifuncptr 是 RedisModuleCtx 的第一个成员这一事实,直接取第一点。

void *getapifuncptr = ((void**)ctx)[0];
RedisModule_GetApi = (int (*)(const char *, void *)) (unsigned long)getapifuncptr;

以下结构显示了 getapifuncptr 是 RedisModuleCtx 的第一个成员这一事实。

struct RedisModuleCtx {
    // getapifuncptr is the first member.
    void *getapifuncptr;            /* NOTE: Must be the first field. */
    struct RedisModule *module;     /* Module reference. */
    client *client;                 /* Client calling a command. */
    
    // ... irrelevant code is omitted ...
};

在弄清楚RM_GetApi是如何导出的之后,让我们来看看RM_GetApi在做什么:

int RM_GetApi(const char *funcname, void **targetPtrPtr) {
    /* Lookup the requested module API and store the function pointer into the
     * target pointer. The function returns REDISMODULE_ERR if there is no such
     * named API, otherwise REDISMODULE_OK.
     *
     * This function is not meant to be used by modules developer, it is only
     * used implicitly by including redismodule.h. */
    dictEntry *he = dictFind(server.moduleapi, funcname);
    if (!he) return REDISMODULE_ERR;
    *targetPtrPtr = dictGetVal(he);
    return REDISMODULE_OK;
}

RM_GetApi的内部实现非常简单——根据要绑定的函数名,在全局哈希表(server.mo duleapi)中找到对应的函数地址,找到后将地址分配给targetPtrPtr。那么 dict 中的内容从何而来呢?

当 Redis 内核启动时,它会通过 moduleRegisterCoreAPI 函数注册其公开的模块 API。具体流程如下:

/* Register all the APIs we export. Keep this function at the end of the
 * file so that's easy to seek it to add new entries. */
void moduleRegisterCoreAPI(void) {
    server.moduleapi = dictCreate(&moduleAPIDictType);
    server.sharedapi = dictCreate(&moduleAPIDictType);

    // Register functions to the global hash table.
    REGISTER_API(Alloc);
    REGISTER_API(TryAlloc);
    REGISTER_API(Calloc);
    REGISTER_API(Realloc);
    REGISTER_API(Free);
    REGISTER_API(Strdup);
    REGISTER_API(CreateCommand);

    // ... irrelevant code is omitted ...
}

其中,REGISTER_API本质上是一个宏定义,由moduleRegisterApi函数在内部实现。moduleRegisterApi 函数将导出的函数名称和函数指针添加到 duleapi server.mo。

int moduleRegisterApi(const char *funcname, void *funcptr) {
    return dictAdd(server.moduleapi, (char*)funcname, funcptr);
}

#define REGISTER_API(name) \
    moduleRegisterApi("RedisModule_" #name, (void *)(unsigned long)RM_ ## name)

那么问题来了——为什么 Redis 要花这么多精力来实现 API 导出绑定机制?理论上,模块动态库中的代码仍然可以通过直接使用动态连接器的符号解析和重定位机制来调用 Redis 公开的可见符号。虽然这是可行的,但会存在符号冲突。例如,如果其他模块也暴露了与 Redis API 相同的函数名称,则依赖于全局符号解析机制和序列来区分(全局符号干预)。另一个原因是 Redis 可以通过这种绑定机制更好地控制不同版本的 API。

最佳实践

入口函数禁用 C++ mangle

从前面的模块加载机制可以看出,模块必须严格保证入口函数名称符合 Redis 的要求。因此,当我们用 C++ 编写模块代码时,我们必须首先禁用 C++ mangle。否则,将报告错误“模块不导出 RedisModule_OnLoad()”。

示例代码如下:

#include "redismodule.h"

extern "C" __attribute__((visibility("default"))) int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {

    // Init code and command register
    
    return REDISMODULE_OK;
}

接管内存统计信息

Redis 需要准确统计数据结构在运行时使用的内存(原子变量 used_memory 用于内部加减),这就要求模块必须使用与 Redis 内核相同的内存分配接口。否则,模块中的内存分配可能不会被计算在内。

REDISMODULE_API void * (*RedisModule_Alloc)(size_t bytes) REDISMODULE_ATTR;
REDISMODULE_API void * (*RedisModule_Realloc)(void *ptr, size_t bytes) REDISMODULE_ATTR;
REDISMODULE_API void (*RedisModule_Free)(void *ptr) REDISMODULE_ATTR;
REDISMODULE_API void * (*RedisModule_Calloc)(size_t nmemb, size_t size) REDISMODULE_ATTR;

对于一些简单的模块,显式调用这些 API 没有问题。但是,对于一些稍微复杂一点的模块,尤其是那些依赖某些第三方库的模块,用模块接口替换库中的所有内存分配就比较困难了。如果我们使用 C++ 来开发 Redis 模块,那么让随处可见的容器分配器(new/delete/make_shared)C++被统一内存分配接管就显得尤为重要了。

new/operator new/placement new

首先,我将解释它们之间的区别:new 是一个关键字,和 sizeof 一样,我们不能修改它的特定功能。新负责三件事:

  1. 分配空间(使用运算符 new)。
  2. 初始化对象(使用 placement new 或 type casts),即调用对象的构造函数。
  3. 返回对象指针。

运算符 new 是可以分配空间的运算符,就像 +/- 一样。我们可以重写它们并修改我们分配空间的方式。

placement new 是运算符 new 的重载形式(即参数形式不同)。例:

void * operator new(size_t, void *location) {  
    return location; 
}

可以看出,要修改 new 使用的默认内存分配,我们可以使用两种方法。

放置 新

它无非是手动模拟关键字 new 的行为。首先,使用模块 API 分配一块内存,然后调用该内存上对象的构造函数。

Object *p=(Object*)RedisModule_Alloc(sizeof(Object));
new (p)Object();

请注意,析构函数还需要特殊处理:

p->~Object();
RedisModule_Free(p);

由于 placement new 没有全局行为,需要手动处理每个对象的分配,因此它仍然无法完全解决复杂 C++ 模块的内存分配问题。

运算符 new

C++ 具有运算符 new 的内置实现。默认情况下,glibc malloc 用于分配内存。C++为我们提供了一个重载机制,即我们可以实现自己的算子 new,并用 RedisModule_Alloc 替换内部的 malloc。

实际上,说运算符 new 重载(同一级别的函数名相同,而参数不同)或重写(派生的函数名和参数必须相同,返回值必须相同,类型协变除外)是不合适的。我认为“覆盖”在这里更合适,因为 C++ 编译器的内置运算符 new 是作为弱符号实现的。以GCC为例:

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
  void *p;

  /* malloc (0) is unpredictable; avoid it.  */
  if (sz == 0)
    sz = 1;

  while (__builtin_expect ((p = malloc (sz)) == 0, false))
    {
      new_handler handler = std::get_new_handler ();
      if (! handler)
  _GLIBCXX_THROW_OR_ABORT(bad_alloc());
      handler ();
    }

  return p;
}

这样,当我们实现一个强符号版本时,它将覆盖编译器自己的实现。

以基本运算符 new/operator delete 为例:

void *operator new(std::size_t size) { 
    return RedisModule_Alloc(size); 
}
void operator delete(void *ptr) noexcept { 
    RedisModule_Free(ptr); 
}

由于运算符 new 具有全局行为,因此可以“一劳永逸”地解决使用 new/delete(make_shared 内部也使用 new)分配内存的所有问题。

跨多个模块的操作员新可见性

由于运算符 new 具有全局可见性(编译器不允许将运算符 new 隐藏在命名空间下),因此如果 Redis 加载多个用 C++ 编写的模块,我们需要注意此行为的影响。

现在假设有两个模块,即 module1 和 module2,其中 module1 重载运算符 new。由于运算符 new 本质上是一个特殊函数,当 module1 被 Redis 加载(使用 dlopen)时,动态连接器会将 module1 实现的运算符 new 函数添加到全局符号表中,因此当加载 module2 并稍后进行符号重定位时,module2 也会将自己运算符 new 链接到 module1 实现的运算符 new。

如果 module1 和 module2 都是我们自己开发的,一般不会有问题。但是,如果 module1 和 module2 是由不同的开发者开发的,或者即使它们都提供了不同的算子新实现,那么只有先加载的实现才会生效(全局合规干预),后面加载的实现的行为可能会异常。

静态链接/动态链接 C++ 标准库

静态链接

有时,我们的模块可能会使用高级 C++ 版本编写和编译。为了防止模块在分发时不被目标平台上对应的 C++ 环境支持,我们通常将 C++ 标准库以静态链接的方式编译到模块中。以Linux平台为例。我们希望将 libstdc++ 和 ibgcc_s静态链接到模块中。通常,如果 Redis 只加载一个 C++ 模块,就不会有问题。但是,如果同时存在两个 C++ 模块,并且采用静态链接 C++ 标准库的方法,则会出现模块异常。具体来说,加载的模块不能正常使用 C++ 流,进而不能正常打印信息、使用正则表达式等(怀疑是 C++ 标准库定义的一些全局变量重复初始化导致此类异常)

动态链接

因此,在此方案中(Redis 加载多个 C++ 库),建议所有模块都使用动态链接。如果还在担心分发时C++版本的兼容性问题,可以将 libstdc++.so 和 ibgcc_s.so 打包在一起,然后使用 $ORIGIN 修改 rpath 来指定指向您版本的链接。

使用块机制提高并发处理能力

Redis 是一种单线程模型(worker 单线程),这意味着 Redis 在执行一个命令时不会处理和响应另一个命令。对于一些耗时的模块命令,我们还是希望这个命令能在后台运行,这样Redis就可以继续读取和处理下一个客户端的命令。

如图 1 所示,cmd1 在 Redis 中执行,并在主线程将 cmd1 放入队列后直接返回(无需等待 cmd1 完成执行)。此时,主线程可以继续处理下一个命令 cmd2。执行 cmd1 后,会再次在主线程中注册一个事件。这样,cmd1 的后续处理就可以在主线程中继续进行,例如将执行结果发送到客户端、写入 AOF、将副本传播到客户端。

2


图1 典型的异步处理模型

虽然块看起来很漂亮,功能强大,但需要小心处理,例如:

• 虽然命令是异步执行的,但仍需要写入 AOF 并同步复制到辅助数据库。如果命令提前写入AOF,并复制到备库,则后续命令执行失败时无法回滚。

• 由于辅助数据库不允许执行块命令,因此主数据库需要将块命令重写为非阻塞命令,并复制到辅助数据库。

• 在异步执行过程中,我们不能只关注打开密钥时的密钥名,因为原始密钥可能在异步线程执行之前就已经被删除了,然后又创建了另一个同名的密钥。也就是说,当前密钥不再是原始密钥。

• 设计块命令是否支持事务和 lua。

• 如果使用线程池,应注意线程池中同一密钥的顺序保留执行(即同一密钥的处理不能乱序)。

避免与其他模块的符号冲突

因为Redis可以同时加载多个模块,而这些模块可能来自不同的团队和个人,所以有一定概率不同的模块会定义相同的函数名。为了避免符号冲突导致的未定义行为,建议每个模块隐藏除 Onload 和 Unload 函数之外的所有符号,并将一些标志实现传递给编译器。如GCC:

-fvisibility=hidden

当心叉子陷阱

用于处理飞行状态的命令

假设该模块使用异步执行模型(请参阅上面的块部分)。当 Redis 执行 AOF rewrite 或 BGSAVE 时,如果 Redis 使用 fork 执行子进程时仍有一些命令处于 inflight 状态,则新生成的基础 AOF 或 RDB 可能不包含正在进行的数据。这似乎不是什么大问题,因为 inflight 的命令在最终完成时也会写入增量 AOF 中。但是,为了兼容 Redis 的原始行为(即分叉时必须没有处于飞行状态的命令,并且处于静态状态),模块最好在分叉之前确保所有处于飞行状态的命令都执行完毕。

在模块中,在分叉之前,我们可以利用 Redis 公开的 RedisModuleEvent_ForkChild 事件来执行我们传递的回调函数。

RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_ForkChild, waitAllInflightTaskFinish);

例如,等待队列在 waitAllInflightTaskFinish 中为空(即执行所有任务):

static void waitAllInflightTaskFinish() {
    while (!thread_pool->idle())
        ;
}

或者,可以通过直接使用glibc暴露的pthread_atfork来达到相同的效果。

int pthread_atfork(void (*prepare)(void), void (*parent)void(), void (*child)(void));
避免死锁

需要注意的是,通过分叉创建的子进程与父进程几乎相同,但并不完全相同。子进程接收父进程的用户级虚拟地址空间的单独副本,包括文本、数据、bss 段、堆和用户堆栈。它还接收与父进程相同的任何打开文件描述符的副本,这意味着它可以读取和写入父进程中的任何打开的文件。父进程和子进程之间的主要区别在于它们具有不同的进程 ID (PID)。

但是,在 Linux 中,分叉时,只有当前线程被复制到子进程。fork(2) - Linux 手册页提供了以下相关说明:

子进程是使用单个线程创建的,该线程名为 fork()。父级的整个虚拟地址空间在子级中复制,包括互斥锁、条件变量和其他 pthreads 对象的状态;使用pthread_atfork(3)可能有助于处理由此可能导致的问题。

换句话说,除了调用 fork 的线程之外,所有其他线程都在子进程中“蒸发”。因此,如果某些异步线程对某些资源持有锁,则子进程中可能会发生死锁,因为这些线程会消失。

解决方案与在飞行中处理相同。确保在分叉之前释放所有锁。(实际上,只要执行了所有处于飞行状态的命令,就会释放通用锁。

确保复制到辅助数据库的 AOF 的幂等性

Redis 中主/辅助复制的主要目的是确保一致性。因此,辅助数据库的唯一任务是无条件地从主数据库接收复制的内容,并保持严格的一致性。但是,需要小心处理一些特殊命令。

在此示例中,Tair 公开的 Tair 字符串支持设置数据的版本号。例如,我们可以编写以下代码:

EXSET key value VER 10

然后,在主数据库执行此命令后,最好在将命令复制到辅助数据库时按如下方式重写该命令:

EXSET key value ABS 11

也就是说,绝对版本号用于强制辅助数据库与主数据库相同。类似的情况还有很多,例如与时间和浮点计算相关的场景。

支持平滑关机

该模块可能会启动一些异步线程或管理一些异步资源。当 Redis 关闭时,需要处理这些资源(例如停止、销毁和写入磁盘)。否则,当 Redis 退出时,可能会发生 coredump。

在 Redis 中,您可以注册 RedisModuleEvent_Shutdown 事件实现。当 Redis 关闭时,它将回调我们传递的 ShutdownCallback。

在较新的 Redis 版本中,该模块也可以通过公开 unload 函数来实现类似的功能。

RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_Shutdown, ShutdownCallback);

避免过大的 AOF

• 实现了AOF文件压缩功能。例如,哈希的所有写入操作都可以重写为一个或多个 hmset 命令。

• 确保单个重写的 AOF 的大小不超过 500 MB。如果超过 500 MB,我们必须将 AOF 重写为多个 CMD,并确保这些 CMD 是否需要以事务方式执行(即确保操作命令的执行是隔离的)。

• 对于结构复杂,无法用现有命令简单重写的模块,可以单独实现 内部 命令,如 xxxload/xxxdump,对模块的数据结构进行序列化和反序列化。该命令不会向客户端公开。

• 如果RedisModule_EmitAOF包含数组类型的参数(即使用“v”标志传递的参数),则数组的长度必须为 size_t 类型。否则,可能会遇到奇怪的错误。

3

RDB 编码具有向后兼容性

RDB 是以二进制格式序列化和反序列化的,因此相对简单。但需要注意的是,如果将来数据结构的序列化方式可能会发生变化,最好添加编解码版本,这样在升级过程中可以保证兼容性。代码如下:

void *xxx_RdbLoad(RedisModuleIO *rdb, int encver) {
  if (encver == version1 ) {
    /* version1 format */
  } else if (encver == version2 ){
    /* version2 format */ 
  }
}

命令实现建议

• 参数 校验 :在执行命令前验证参数的有效性(如参数的正确数量和类型),在命令执行不成功时尽量避免提前修改密钥空间(如提前使用RedisModule_ModuleTypeSetValue修改主库)。

错误消息 :返回的错误消息应简单明了,说明错误类型。

• 一致的响应类型:命令的返回类型在不同情况下应该是一致的,例如当密钥不存在时、密钥 类型 错误、执行成功、某些参数错误等。通常,除错误类型(例如简单字符串或数组)外,所有情况都应返回相同的类型,例如简单字符串或数组(即使它是空数组)。这使客户端更容易分析命令返回值。

• 检查读写类型:命令必须严格区分 读写类型 ,因为它决定了命令是否可以在副本上执行,以及命令是否需要同步写入 AOF。

• 复制幂等性和 AOF:对于写入命令,请使用 RedisModule_ReplicateVerbatim 或 RedisModule_Replicate 执行主/辅助复制并写入 AOF (必要时重写原始命令)。Multi/exec 会在 RedisModule_Replicate 生成的 AOF 之前和之后自动添加(以确保模块中生成的命令是隔离的)。因此,建议优先使用 RedisModule_ReplicateVerbatim 进行复制和写入 AOF。但是,如果命令中有版本号等参数,请使用 RedisModule_Replicate 将版本号重写为绝对版本号,将过期时间重写为绝对过期时间。此外,如果需要使用 RedisModule_Replicate 重写命令,请确保不会再次重写重写的命令。

复用 argv 参数:传递给命令的 argv 中的参数类型为 RedisModuleString **,命令返回后会自动释放这些 RedisModuleString 指针。因此,不应在命令中直接引用这些 RedisModuleString 指针。如果需要这样做(例如避免内存复制),可以使用 RedisModule_RetainString/RedisModule_HoldString 来增加 RedisModuleString 的引用计数,但请记住稍后手动释放它们。

开钥匙的方式 :用RedisModule_OpenKey开钥匙时,要严格区分REDISMODULE_READ和REDISMODULE_WRITE两种开门方式。不区分会影响内部stat_keyspace_misses和stat_keyspace_hits信息的更新,以及过期的重写。同时,无法删除使用 REDISMODULE_READ 方法打开的密钥,否则会报错。

• 不同键类型的处理方式:目前只有字符串的set命令可以强制覆盖其他 类型的键 。当键存在但类型不匹配时,其他命令应返回错误“WRONGTYPE Operation against a key of having the wrong kind value”。

• 集群支持多键命令:对于多键命令,firstkey、lastkey 和 keystep 的值必须正确处理,因为只有当这些值正确时,Redis 才能检查这些 键在集群 模式下是否存在 CROSS SLOTS 问题。

• 全局索引 和结构 :如果模块有自己的全局索引,请检查索引中是否包含 dbid、key 等信息。Redis 的 move、rename、swapdb 等命令可以暗中更改密钥名称并交换两个 dbid。因此,如果此时未同步更新索引,则可能会出现意外错误。

根据角色确定操作 :Redis 模块可以是主数据库,也可以是辅助数据库。该模块可以使用RedisModule_GetContextFlags来确定当前的 Redis 角色,并根据角色采取不同的操作(例如是否主动过期)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何使用 C++ 开发 Redis 模块 的相关文章

  • 获取数组变量的地址是什么意思?

    今天我读到了一段让我很困惑的 C 代码片段 include
  • System.IO.IOException:由于意外>数据包格式,握手失败?

    有谁知道这意味着什么 System Net WebException 底层连接已关闭 发送时发生意外错误 gt System IO IOException 由于意外 握手失败 数据包格式 在 System Net Security SslS
  • 将字符串中的“奇怪”字符转换为罗马字符

    我需要能够将用户输入仅转换为 a z 罗马字符 不区分大小写 所以 我感兴趣的角色只有26个 然而 用户可以输入他们想要的任何 形式 的字符 西班牙语 n 法语 e 和德语 u 都可以包含用户输入中的重音符号 这些重音符号会被程序删除 我已
  • 从 C 结构生成 C# 结构

    我有几十个 C 结构 我需要在 C 中使用它们 典型的 C 结构如下所示 typedef struct UM EVENT ULONG32 Id ULONG32 Orgin ULONG32 OperationType ULONG32 Size
  • mprotect 之后 malloc 导致分段错误

    在使用 mprotect 保护内存区域后第一次调用 malloc 时 我遇到分段错误 这是执行内存分配和保护的代码片段 define PAGESIZE 4096 void paalloc int size Allocates and ali
  • 如何使用MySqlCommand和prepare语句进行多行插入?(#C)

    Mysql 给出了如何使用准备语句和 NET 插入行的示例 http dev mysql com doc refman 5 5 en connector net programming prepared html http dev mysq
  • libxml2 xmlChar * 到 std::wstring

    libxml2似乎将所有字符串存储在 UTF 8 中 如xmlChar xmlChar This is a basic byte in an UTF 8 encoded string It s unsigned allowing to pi
  • C++ 插件的“最适合”动态类型匹配

    我有一个几乎所有东西都是插件的架构 该架构以图形用户界面为基础 其中每个插件都由一个 表面 即用户可以通过其与插件交互的 UI 控件 表示 这些表面也是插件 每当添加新插件时 瘦主机都会自动确定哪个可用表面与其最匹配的 UI 如何在 C 中
  • 使用 WF 的多线程应用程序的错误处理模式?

    我正在写一个又长又详细的问题 但只是放弃了它 转而选择一个更简单的问题 但我在这里找不到答案 应用程序简要说明 我有一个 WPF 应用程序 它生成多个线程 每个线程执行自己的 WF 处理线程和 WF 中的错误 允许用户从 GUI 端进行交互
  • C++ 错误 - “成员初始值设定项表达式列表被视为复合表达式”

    我收到一个我不熟悉的 C 编译器错误 可能是一个非常愚蠢的错误 但我不能完全指出它 Error test cpp 27 error member initializer expression list treated as compound
  • asp.net网格分页的SQL查询

    我在用iBatis and SQLServer 使用偏移量和限制进行分页查询的最佳方法是什么 也许我添加该列ROW NUMBER OVER ORDER BY Id AS RowNum 但这只会阻止简单查询的数据访问 在某些情况下 我使用选择
  • 为什么要在 C++ 中使用 typedef?

    可以说我有 set
  • ASP.NET JQuery AJAX POST 返回数据,但在 401 响应内

    我的应用程序中有一个网页 需要调用我设置的 Web 服务来返回对象列表 这个调用是这样设置的 document ready function var response ajax type POST contentType applicati
  • 0-1背包算法

    以下 0 1 背包问题是否可解 浮动 正值和 浮动 权重 可以是正数或负数 背包的 浮动 容量 gt 0 我平均有 这是一个相对简单的二进制程序 我建议用蛮力进行修剪 如果任何时候你超过了允许的重量 你不需要尝试其他物品的组合 你可以丢弃整
  • 使用 iTextSharp 5.3.3 和 USB 令牌签署 PDF

    我是 iTextSharp 和 StackOverFlow 的新手 我正在尝试使用外部 USB 令牌在 C 中签署 PDF 我尝试使用从互联网上挖掘的以下代码 Org BouncyCastle X509 X509CertificatePar
  • 使用 HTMLAgilityPack 从节点的子节点中选择所有

    我有以下代码用于获取 html 页面 将网址设置为绝对 然后将链接设置为 rel nofollow 并在新窗口 选项卡中打开 我的问题是关于将属性添加到 a s string url http www mysite com string s
  • C 中带有指针的结构的内存开销[重复]

    这个问题在这里已经有答案了 我意识到当我的结构包含指针时 它们会产生内存开销 这里有一个例子 typedef struct int num1 int num2 myStruct1 typedef struct int p int num2
  • Visual Studio 2017 完全支持 C99 吗?

    Visual Studio 的最新版本改进了对 C99 的支持 最新版本VS2017现在支持所有C99吗 如果没有 C99 还缺少哪些功能 No https learn microsoft com en us cpp visual cpp
  • C语言声明数组没有初始大小

    编写一个程序来操纵温度详细信息 如下所示 输入要计算的天数 主功能 输入摄氏度温度 输入功能 将温度从摄氏度转换为华氏度 独立功能 查找华氏度的平均温度 我怎样才能在没有数组初始大小的情况下制作这个程序 include
  • 服务器响应 PASV 命令返回的地址与建立 FTP 连接的地址不同

    System Net WebException 服务器响应 PASV 命令返回的地址与建立 FTP 连接的地址不同 在 System Net FtpWebRequest CheckError 在 System Net FtpWebReque

随机推荐

  • WordPress主题 响应式个人博客主题Kratos源码

    Kratos 是一款专注于用户阅读体验的响应式 WordPress 主题 整体布局简洁大方 针对资源加载进行了优化 Kratos主题基于Bootstrap和Font Awesome的WordPress一个干净 简单且响应迅速的博客主题 Vt
  • PCB问题:Dummy NetPoint on shape:To suppress in report attach OK UNASSIGNED Shape解决方法

    问题 Total shapes not on a net To suppress in report attach OK UMASSIGMED SHAFE pr operty to shape 该问题一般都是在删除铜或者修铜时留下的 解决方
  • 深度学习中的KL散度

    1 KL散度概述 KL散度 Kullback Leibler Divergence 也称为相对熵 是信息论中的一个概念 用于衡量两个概率分布间的差异 它起源于统计学家Kullback和Leibler的工作 它的本质是衡量在用一个分布来近似另
  • 计算机毕设项目分享(含算法) 源码+论文(一)

    1 基于opencv的图像增强算法系统 直方图均衡化是通过调整图像的灰阶分布 使得在0 255灰阶上的分布更加均衡 提高了图像的对比度 达到改善图像主观视觉效果的目的 对比度较低的图像适合使用直方图均衡化方法来增强图像细节 使用中心为5的8
  • Gateway网关-路由的过滤器配置

    目录 一 路由过滤器 GatewayFilter 1 1 过滤器工厂GatewayFilterFactory 1 2 案例给所有进入userservice的请求添加一个请求头 Truth itcastis freaking awesome
  • 用一个简单的例子教你如何 自定义ASP.NET Core 中间件(一)

    提起中间件大家一定不陌生 我们也用过 NET core很多很好用的中间件 但是如何自己写一个中间件呢 可能大部分同学不清楚怎么写 我之前也不会 看了微软官方文档 ASP NET Core 中间件 感觉讲的也不是很清楚 下面就用一个简单的例子
  • 论文阅读:DSformer:A Double Sampling Transformer for Multivariate Time Series Long-term Prediction

    DSformer A Double Sampling Transformer for Multivariate Time Series Long term Prediction 一篇发表在CIKM 2023上的基于transformer的时
  • pthread_detach(pthread_self())是一个用于将当前线程设置为分离状态的函数调用

    pthread detach pthread self 是一个用于将当前线程设置为分离状态的函数调用 具体解释如下 pthread self pthread self 是一个pthread库中的函数 用于获取当前线程的线程ID pthrea
  • Jmeter吞吐量控制器使用小结

    吞吐量控制器 Throughput Controller 场景 在同一个线程组里 有10个并发 7个做A业务 3个做B业务 要模拟这种场景 可以通过吞吐量 模拟器 来实现 添加吞吐量控制器 用法1 Percent Executions 在一
  • 【实战详解】如何快速搭建接口自动化测试框架?Python + Requests

    摘要 本文主要介绍如何使用Python语言和Requests库进行接口自动化测试 并提供详细的代码示例和操作步骤 希望能对读者有所启发和帮助 前言 随着移动互联网的快速发展 越来越多的应用程序采用Web API 也称为RESTful API
  • 详解 Jeecg-boot 框架如何配置 elasticsearch

    目录 一 下载安装 Elasticsearch 1 地址 https www elastic co cn downloads elasticsearch 2 下载完成后 解压缩 进入config目录更改配置文件 3 修改配置完成后 前往bi
  • 20231218_144100 java jdbc的dml操作 实现增删改的功能

    导入jar包 在项目目录下新建lib目录 在lib目录下存放驱动jar包 让项目识别lib目录 让项目知道这个lib目录是库目录 在lib目录上右击 点选 add as library 然后确定 验证lib目录是否设置成功 设置lib目录前
  • onnx 图像分类

    参考文章 netron 模型可视化工具netron CSDN博客 Pytorch图像分类模型部署 ONNX Runtime本地终端推理 哔哩哔哩 bilibili 使用netron可视化模型结构 1 使用在线版 浏览器访问 Netron 点
  • 多用户无线信道资源分配算法优化

    随着无线通信技术的快速发展 越来越多的用户依赖于无线网络进行通信和数据传输 然而 由于无线信道资源的有限性 多用户之间的信道资源分配变得尤为重要 为了提高无线通信的效率和公平性 研究者们一直致力于优化多用户无线信道资源分配算法 本文将介绍多
  • 世微 AP2400 降压恒流驱动ic 全亮 半亮 爆闪三功能循环模式

    产品描述 AP2400 是一款 PWM 工作模式 高效率 外围简单 外驱功率管 适用于 5 100V输入的高精度降压 LED 恒流驱动芯片 外驱 MOS 最大输出电流可达 6A AP2400 可实现三段功能切换 通过MODE1 2 3 切换
  • Python环境搭建

    一 Python运行环境 PATH添加环境变量 方便添加后续工具 后续自定义安装即可 交互式解释器 二 开发环境 PyCharm
  • SSM框架实现学生信息管理系统

    这个管理系统是我学完SSM后的一个练手小项目 感兴趣的小伙伴可以在B站搜下SSM实战 这是雷神讲的一套课程 他用得是JSP进行前端页面渲染 前端方面的湘学习占了 但是JSP技术太老了 我把JSP改成Thymeleaf 有关Thymeleaf
  • 使用C语言设计并实现一个成绩管理系统

    使用C语言设计并实现一个成绩管理系统 该系统用于教师管理一门课程的成绩 系统功能 成绩录入 打印成绩单 修改成绩 统计分数段 统计平均分 统计不及格学生 相关要求 1 系统要有主菜单界面 让教师了解系统的功能 以及如何选择系统功能 2 系统
  • .h5文件简介

    一 简介 HDF5 Hierarchical Data Format version 5 是一种用于存储和组织大量数据的文件格式 它支持高效地存储和处理大规模科学数据的能力 HDF5 是一种灵活的数据模型 可以存储多种数据类型 包括数值数据
  • 如何使用 C++ 开发 Redis 模块

    在本文中 我将总结 Tair 在使用 C 开发 Redis 模块时遇到的一些问题 并将其提炼为最佳实践 目的是为 Redis 模块的用户和开发人员提供帮助 其中一些最佳实践也可以应用于 C 编程语言和其他编程语言 介绍 从 Redis 5