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

阿里妹导读

本文将试着总结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复制等操作。

图1 典型的异步处理模型

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类型,否则可能会遇到诡异的错误;

    

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 Page:http://linux.die.net/man/2/fork