最佳实践|如何使用c++开发redis module

本文涉及的产品
云数据库 MongoDB,独享型 2核8GB
推荐场景:
构建全方位客户视图
云数据库 Tair(兼容Redis),内存型 2GB
云原生多模数据库 Lindorm,多引擎 多规格 0-4节点
简介: 本文将试着总结Tair用c++开发redis module中遇到的一些问题并沉淀为最佳实践,希望对redis module的使用者和开发者带来一些帮助(部分最佳实践也适用于c和其他语言)。

简介

Redis在5.0版本开始支持以module插件的方式来扩展redis的能力,包括但不限于开发新的数据结构、实现命令监听和过滤、扩展新的网络服务等。可以说,module的出现极大的扩展了redis的灵活性,也大大的降低了redis的开发难度。

目前为止,redis社区已经涌现了很多module,覆盖了不同领域,生态已经丰富起来了。它们之中大多都是使用c语言开发。但是,redis module也支持使用其他语言开发,如c++和 rust等。本文将试着总结Tair用c++开发redis module中遇到的一些问题并沉淀为最佳实践,希望对redis module的使用者和开发者带来一些帮助(部分最佳实践也适用于c和其他语言)。

原理

Redis内核使用c语言开发,因此在c环境下开发类似插件的东西很容易想到动态链接库。redis的确是这么做的,但是有几个地方需要注意:

1.Redis内核会暴露出/导出很多API给module使用(如内存分配接口、redis核心db结构的操作接口),注意这些API是redis自己解析绑定的,而不是靠动态连接器解析的。

2.Redis内核使用dlopen显示的装载module,而不是直接交由动态链接器隐式装载。即module需要实现特定的接口,redis会自动调用module的入口函数,完成一些API初始化、数据结构注册等功能。

加载

Redis内核中关于module加载的逻辑部分代码如下(代码位于module.c中):

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

    // 打开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;
    }

    // 获取module中的onload函数符号地址
    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. */
    // 调用onload对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. */

    //... 无关代码省略 ...

    moduleFreeContext(&ctx);
    return C_OK;
}

API 绑定

在module的初始化函数中,需要显示的调用RedisModule_Init初始化redis内核导出的api。比如:

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

    // ... 无关代码省略 ...
}

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;

    // 绑定redis导出的api
    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);
  
    // ... 无关代码省略 ...
}

先看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内核在调用module的OnLoad函数时,通过RedisModuleCtx传递了RedisModule_GetApi函数地址。可以看上文加载module部分代码,在调用Onload函数之前,redis使用moduleCreateContext初始化了一个RedisModuleCtx并传递给module。

在moduleCreateContext中,会将redis内部定义的RM_GetApi函数地址赋值给RedisModuleCtx的getapifuncptr成员。

void moduleCreateContext(RedisModuleCtx *out_ctx, RedisModule *module, int ctx_flags) {
    memset(out_ctx, 0 ,sizeof(RedisModuleCtx));
    // 这里把GetApi地址传递给module
    out_ctx->getapifuncptr = (void*)(unsigned long)&RM_GetApi;
    out_ctx->module = module;
    out_ctx->flags = ctx_flags;

    // ... 无关代码省略 ...
}

因此,在module中就可以通过RedisModuleCtx来获取GetApi函数了。那么这里为什么不能直接使用ctx->getapifuncptr获取而要使用((void**)ctx)[0]这种“奇怪”的方式呢?原因是,RedisModuleCtx本身是一个定义在redis内核中的数据结构,其内部结构对module而言是不可见的(opaque pointer)。因此,这里只能使用一种hack的方式,巧用getapifuncptr是RedisModuleCtx第一个成员这个特点,直接取第一个指针即可。

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

下面的结构展示了getapifuncptr是RedisModuleCtx第一个成员的事实。

struct RedisModuleCtx {
    // getapifuncptr是第一个成员
    void *getapifuncptr;            /* NOTE: Must be the first field. */
    struct RedisModule *module;     /* Module reference. */
    client *client;                 /* Client calling a command. */
    
    // ... 无关代码省略 ...
};

搞清楚了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.moduleapi)中查找对应的函数地址,找到了就把地址赋值给targetPtrPtr。那么dict中的内容哪里来的?

Redis内核在启动的时候,会通过moduleRegisterCoreAPI函数注册自身暴露的module 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_API(Alloc);
    REGISTER_API(TryAlloc);
    REGISTER_API(Calloc);
    REGISTER_API(Realloc);
    REGISTER_API(Free);
    REGISTER_API(Strdup);
    REGISTER_API(CreateCommand);

    // ... 无关代码省略 ...
}

其中REGISTER_API本质也是一个宏定义,内部通过moduleRegisterApi函数实现,而moduleRegisterApi函数内部就会把导出的函数名和函数指针添加到server.moduleapi中。

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导出绑定机制呢?理论上,直接利用动态连接器的符号解析和重定位机制,module这些动态库中的代码依然可以调用到redis暴露的可见符号的。这样虽然可行,但是会存在符号冲突的问题,比如其他的module也暴露了一个和redis api一样的函数名,那么这个时候就依赖于全局的符号解析机制和顺序了(全局符号介入)。还有一个原因,redis可以通过这个bind机制更好的控制api的不同版本。

一些最佳实践

入口函数禁用c++ mangle

由前面的module加载机制可以看出,module内部的必须严格保证入口函数名和redis要求的一致。因此,当我们使用c++编写module代码时,首先必须禁用c++ mangle,否则将报“Module does not export 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加加减减),这就要求module内部必须和redis核心内部使用相同的内存分配接口,否则就可能会导致module内的内存分配无法被统计到的问题。

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;

对于一些简单的module而言,显示的调用这些api没有什么问题。但是对于一些稍微复杂的,特别是会依赖一些第三方库的module而言,要想把库里面所有的内存分配全部替换为module接口,就比较困难了。更甚者,如果我们使用c++来开发redis module,那么如何让c++中随处可见的new/delete/make_shared/各种容器分配器也被统一内存分配接管,就显得更为重要了。

new/operator new/placement new

首先阐述一下他们的区别:new是一个关键字,和sizeof一样,我们无法修改其具体功能。new主要做三件事:

1.分配空间(使用operator new)

2.初始化对象(使用placement new或者类型强转),即调用对象的构造函数

3.返回对象指针

operator new是一个操作符,和 +/- 操作符一样,作用是分配空间。我们可以重写它们,修改分配空间的方式。

placement new是operator new的一种重载形式(即参数形式不同)。比如:

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

可见,要想实现修改new默认使用的内存分配,我们可以使用两种方式。

placement new

无非就是手动模拟关键字new的行为,先使用module api分配好一块内存,然后在这个内存上调用对象的构造函数。

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

同时注意析构时也需要特殊处理:

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

因为placement new不具有全局行为,需要手动处理每个对象的分配,因此对于复杂的c++ module而言依然不能彻底解决内存分配的问题。

operator new

c++内置了operator new的实现,默认使用glibc malloc分配内存。c++给我们提供了重载机制,即我们可以实现自己的operator new,将内部的malloc替换为RedisModule_Alloc即可。

其实说operator new是重载(同层级函数名相同参数不同)或重写(派生层级函数名和参数必须相同,返回值除了类型协变之外也必须相同)都不太合适,我感觉这里使用覆盖更贴切。因为c++编译器内置的operator new被实现为一个弱(weak)符号,以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;
}

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

以最基本的operator new/operator delete为例:

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

因为operator new具有全局行为,因此这样可以“一劳永逸”的解决所有使用new/delete(make_shared内部也是使用new)分配内存的问题。

operator new在多个module之间的可见性

因为operator new具有全局可见性(编译器也不允许将operator new放入一个namespace下隐藏),因此如果redis加载不止一个c++编写的module,那么就需要小心这种行为的影响。

现在假设有两个module分别为module1何module2,其中module1自己重载了operator new, 由于operator new本质就是一个特殊的函数,当module1被redis加载时(使用dlopen),动态连接器会把module1实现的operator new函数加入到全局符号表里,因此后续在加载module2并进行符号重定位时,module2也会将自己的operator new链接到module1实现的operator new上。

如果module1和module2都是我们自己开发的,这一般不会有什么问题。但是如果module1和module2分数不同的开发者,更甚者它们都提供了不同的operator new实现,那么只有先加载的module的实现会生效(全局符合介入),后加载的module的行为将可能出现异常。

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

静态链接

有时候,我们的module可能使用较高的c++版本编写和编译,为了防止module在分发时目标平台上没有对应的c++环境支持,我们通常会将c++标准库以静态链接的方式编译进module中。以linux平台为例,我们想将libstdc++和ibgcc_s静态链接到mdoule中。通常,如果redis只加载一个c++ module这一搬不会有什么问题。但是如果同时有两个c++ moudle并同时采用了静态链接c++标准库的方式,那么这可能会导致module异常。具体表现为后加载的moudle内部无法正常的使用c++ stream,进而表现为无法正常的打印信息、使用正则表达式等(怀疑和c++标准库自己定义的一些全局变量被重复初始化导致)。

该问题已经在gcc上存在多年:https://gcc.gnu.org/bugzilla//show_bug.cgi?id=68479

动态链接

因此,在这种场景下(redis会加载一个以上的c++库),还是建议module都使用动态链接的方式。如果还是担心分发时c++版本的兼容问题,那么可以将libstdc++.so和ibgcc_s.so等一起打包,然后使用$ORIGIN修改rpath指定链接自己的版本即可。

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

redis是单线程模型(指worker单线程), 这意味着redis在执行一个命令的时候,不会处理并响应另一个命令。而对于一些比较耗时的module命令,我们还是希望这个命令可以后台运行,这样redis可以继续读取并处理下一个客户端的命令。

如图1所示,cmd1在进入redis中执行,在主线程把cmd1放入队列之后就直接返回了(不会等待cmd1执行结束),此时主线程可以继续处理下一个命令cmd2。当cmd1被执行完毕之后,会重新向主线程中注册一个事件,从而可以在主线程中继续cmd1的后续处理,比如向客户端发送执行结果、写AOF以及向replica复制等操作。

image.png

block虽然看上去很美好很强大,但是需要小心处理一些坑,如:

  • 命令虽然异步执行了,但是写AOF和向备库复制依然同步做。如果提前写AOF并向备库复制,万一后面命令执行失败了就无法回滚;
  • 因为备库是不允许执行block命令的,因此主库需要将block类型的命令rewrite成非block类型的命令复制给备库;
  • 异步执行时,在open一个key时不能只看keyname,因为可能在异步线程执行之前,原来的key已经被删除了,然后又有一个同名的key被创建,即当前看到的key已经不是原来的key了;
  • 设计好block类型的命令是否支持事务和lua;
  • 如果采用线程池,需要注意相同key在线程池中的保序执行问题(即相同key的处理不能乱序);

避免和其他Module符号冲突

因为redis可以同时加载多个module,这些module可能来自不同的团队和个人,因此存在一定的概率,不同的module会定义相同的函数名。为了避免符号冲突导致的未定义行为,建议每个module都把除了Onload和Unload函数之外的符号都隐藏掉,可以在给编译器传递一些flag实现。如gcc:

-fvisibility=hidden

小心Fork陷阱

处理inflight状态的命令

如果module采用异步执行模型(参看前文block一节),那么当redis做aofrewrite或bgsave时,在redis fork子进程的瞬间,如果还有一些命令处于inflight状态,那么此时新产生的base aof或者rdb可能并不会包含这些inflight时的数据,虽然这个看上去也没有太大问题,因为inflight的命令最终完成时也会把命令写入增量的aof中。但是,为了和redis原来的行为兼容(即fork时一定没有处于inflight状态的命令,是一个静止的状态),module最好还是保证所有的inflight状态的命令都执行完了再执行fork。

在module中可以通过redis暴露的RedisModuleEvent_ForkChild事件,在fork执行之前执行一个我们传入的回调函数。

RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_ForkChild, waitAllInflightTaskFinish);

比如在waitAllInflightTaskFinish中等待队列为空(即所有task都执行结束):

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

或者,直接使用glibc暴露的pthread_atfork也能实现同样的效果。

int pthread_atfork(void (*prepare)(void), void (*parent)void(), void (*child)(void));

避免死锁

我们知道通过fork创建的一个子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝,包括文本、数据和bss段、堆以及用户栈等。子进程还获得与父进程任何打开文件描述符相同的拷贝,这就意味着子进程可以读写父进程中任何打开的文件,父进程和子进程之间最大的区别在于它们有着不同的PID。

但是有一点需要注意的是,在Linux中,fork的时候只复制当前线程到子进程,在fork(2)-Linux Man Page中有着这样一段相关的描述:

The child process is created with a single thread--the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.

也就是说除了调用fork的线程外,其他线程在子进程中“蒸发”了。

因此,如果在一些异步线程中持有了一些资源的锁,那么在子进程中,因为这些线程消失了,那么子进程可能会发生死锁的问题。

解决方法和解决inflight一样,保证在fork之前所有的锁都释放掉即可。(其实只要所有inflight状态的命令都执行完了,一般锁也就都释放了)

确保向备库复制的AOF保持语义幂等

Redis的主备复制首要目标就是保证主备的一致性。因此备库要做的就是无条件接收来自主库的复制内容,并严格保持一致。但是对于一些比较特殊的命令而言,需要小心处理。

以Tair暴露的Tair String为例,支持给数据设置版本号,比如用户写入:

EXSET key value VER 10

那么主库在执行这条命令之后,最好在向备库复制时将命令改写为:

EXSET key value ABS 11

即使用绝对值版本号强行让备库和主库一致。类似的案例还有很多,比如和时间相关、和浮点计算相关等场景。

支持graceful shutdown

Module内部可能会启动一些异步线程或者管理一些异步资源,这些资源需要在redis shutdown时被处理(如停止、析构、写磁盘等),否则redis在退出时可能发生coredump。

在redis中,可以注册RedisModuleEvent_Shutdown事件实现,当redis关机时会回调我们传入的ShutdownCallback。

当然,在较新的redis版本中,module也可以通过暴露unload函数来实现类似的功能。

RedisModule_SubscribeToServerEvent(ctx, RedisModuleEvent_Shutdown, ShutdownCallback);

避免过大的AOF

  • 实现aof文件压缩功能,如将一个hash的所有写操作重写为一条hmset命令(也可能是多条);
  • 避免重写后的一条aof过大(如超过500MB),如果超过,则需要rewrite成多条cmd,同时需要确保这些多条cmd是否需要以事务的方式执行(即需要操作命令执行的隔离性);
  • 对于一些复杂结构,无法简单重写为已有命令的module,可以单独实现一个“内部”命令,如xxxload/xxxdump等,用于实现对该module数据结构的序列化和反序列化,该命令不会对外暴露给客户端;
  • RedisModule_EmitAOF中如果包含array类型的参数(即使用'v' flag传递的参数),则array的长度一定要使用size_t类型,否则可能会遇到诡异的错误;

image.png

RDB编码具有向后兼容能力

RDB是二进制格式的序列化和反序列化,因此相对而言比较简单。但是需要注意的是,如果数据结构以后的序列化方式可能会改变,则最好加上编解码的版本,这样在升级的时候可以保证兼容性,如下:

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

一些命令实现的建议

  • 参数检验:尽量在命令开始处对参数合法性(如参数个数是否正确、参数类型是否正确等)进行校验,尽量避免命令没有成功执行的情况下提前污染了keyspace(如提前使用了RedisModule_ModuleTypeSetValue修改主数据库)
  • 错误信息:返回的错误信息应尽可能简单明了,阐明错误类型是什么
  • 响应类型保持统一:注意命令在各种情况下的返回类型要统一,如key不存在、key类型错误、执行成功以及一些参数错误时的响应类型。通常情况下,除了返回错误类型之外,其他的所有情况都应该返回相同类型,如都返回一个简单字符串、或者都返回一个数组(哪怕是一个空数组)。这样客户端在解析命令返回值时比较方便
  • 确认读写类型:命令应严格区分读写类型,这涉及到该命令能否在replica上执行、以及该命令是否需要进行同步、写aof等
  • 复制幂等性和AOF:对于写命令,需要自行使用RedisModule_ReplicateVerbatim或者RedisModule_Replicate进行主备复制和写AOF(必要的时候需要对原命令进程重写)。其中,使用RedisModule_Replicate产生的AOF,前后都会被自动加上multi/exec(保证module内产生的命令具有隔离性)。因此,推荐优先使用RedisModule_ReplicateVerbatim进行复制和写AOF。但是,如果命令中存在诸如版本号等参数,则必须使用RedisModule_Replicate将版本号重写为绝对版本号,将过期时间重写为绝对过期时间。另外,如果一个命令最终RedisModule_Replicate对命令进行重写,则需要保证重写后的命令不会再次发生重写。
  • 复用argv参数:命令传入的argv中的参数类型为RedisModuleString ** ,这些RedisModuleString在命令返回后会被自动Free掉,因此命令中不应该直接引用这些RedisModuleString指针,如果非要这么做(如避免内存拷贝),可以使用RedisModule_RetainString/RedisModule_HoldString增加该RedisModuleString的引用计数,但是之后一定要记得自己手动Free
  • key打开方式:在使用RedisModule_OpenKey打开一个key的时候,要严格区分打开的类型:REDISMODULE_READ、REDISMODULE_WRITE,因为这影响着是否更新内部的stat_keyspace_misses和stat_keyspace_hits信息,还影响的了过期再写入的问题。同时,使用REDISMODULE_READ方式打开的key不能被删除,否则报错
  • key类型处理:目前只有string的set命令可以强行覆盖其他类型的key,其他的命令在遇到key存在但类型不匹配时需要返回""WRONGTYPE Operation against a key holding the wrong kind of value"错误
  • 多key命令的cluster支持对于多key的命令,一定要处理好firstkey、lastkey、keystep这三个值,因为只有这三个值对了,在cluster模式下,redis才会去检查这些key是否存在CROSS SLOTS的问题
  • 全局索引、结构module中如果有自己维护的全局索引,需要谨慎索引中是否包含dbid、key等信息,因为redis的move、rename、swapdb等命令会“偷梁换柱”式的更换key的名字、交换两个dbid,因此此时如果索引没有同步更新,将得到意想不到的错误
  • 根据角色来确定动作module本身运行的redis可能是一个主也可能是一个备,module内部可以使用RedisModule_GetContextFlags来判断当前redis的角色,并根据不同的角色来采取不同的行为(如是否进行主动过期处理等)


总结

Tair当前支持了非常多的扩展数据结构(其中redis 5.x企业版使用module方式,Tair自研企业版 6.x使用builtin方式),基本涵盖了各种应用场景(具体见介绍文档),其中既有像TairString和TairHash等小而美的数据结构(已经开源),也有像Tair Search和Vector等更为复杂和强大的计算型数据结构,充分满足AIGC背景下各种业务场景,欢迎使用。

介绍文档:https://help.aliyun.com/zh/redis/developer-reference/extended-data-structures-of-apsaradb-for-redis-enhanced-edition

fork(2)-Linux Man Pagehttp://linux.die.net/man/2/fork


作者 | 驱动

来源 | 阿里云开发者公众号


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
WK
|
1月前
|
机器学习/深度学习 人工智能 算法
那C++适合开发哪些项目
C++ 是一种功能强大、应用广泛的编程语言,适合开发多种类型的项目。它在游戏开发、操作系统、嵌入式系统、科学计算、金融、图形图像处理、数据库管理、网络通信、人工智能、虚拟现实、航空航天等领域都有广泛应用。C++ 以其高性能、内存管理和跨平台兼容性等优势,成为众多开发者的选择。
WK
68 1
|
2月前
|
Rust 资源调度 安全
为什么使用 Rust over C++ 进行 IoT 解决方案开发
为什么使用 Rust over C++ 进行 IoT 解决方案开发
77 7
|
2月前
|
NoSQL Java Redis
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
这篇文章介绍了如何使用Spring Boot整合Apache Shiro框架进行后端开发,包括认证和授权流程,并使用Redis存储Token以及MD5加密用户密码。
36 0
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
WK
|
1月前
|
开发框架 移动开发 Java
C++和Java哪个更适合开发移动应用
本文对比了C++和Java在移动应用开发中的优劣,从市场需求、学习难度、开发效率、跨平台性和应用领域等方面进行了详细分析。Java在Android开发中占据优势,而C++则适合对性能要求较高的场景。选择应根据具体需求和个人偏好综合考虑。
WK
52 0
WK
|
1月前
|
安全 Java 编译器
C++和Java哪个更适合开发web网站
在Web开发领域,C++和Java各具优势。C++以其高性能、低级控制和跨平台性著称,适用于需要高吞吐量和低延迟的场景,如实时交易系统和在线游戏服务器。Java则凭借其跨平台性、丰富的生态系统和强大的安全性,广泛应用于企业级Web开发,如企业管理系统和电子商务平台。选择时需根据项目需求和技术储备综合考虑。
WK
67 0
|
2月前
|
NoSQL API Redis
如何使用 C++ 开发 Redis 模块
如何使用 C++ 开发 Redis 模块
|
16天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
27 2
|
22天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
55 5
|
28天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
59 4
|
29天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
71 4

相关产品

  • 云数据库 Tair(兼容 Redis)