Redian新闻
>
最佳实践|如何使用c++开发redis module

最佳实践|如何使用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

阿里云开发者社区,千万开发者的选择


阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
桡动脉置管的 10 个最佳实践技巧百度百舸平台的大模型训练最佳实践首个Unified Redis Release,Redis影响最深远的版本发布ESG践行进一步深化,创业邦2023创投机构ESG最佳实践奖重磅发布5123 血壮山河之武汉会战 富金山战役 11面试官:如何使用Dockerfile去构建自定义的Docker镜像?问倒一堆从理论到实践,学习中间件的最佳实践方法 | 极客时间面试技巧|如何回答Why should we hire you?学会地把自己说成Hiring Manager不得不雇的人才!Kubernetes 资源请求和限制的最佳实践中国连锁经营协会:2023 CCFA零售业供应链最佳实践案例集普京開戰與拜登的盤算云原生场景下高可用架构的最佳实践8 个线程池最佳实践和坑!使用不当直接生产事故!!Good News For Cold Noodle Lovers: License Requirements Relaxed追忆半个世纪前的苏州一日游【老键曲库】Sad sad, Sinead O\'Connor diedRedis实战 | 使用Redis 的有序集合(Sorted Set)实现排行榜功能,和Spring Boot集成AIGC 风口下,一窥智能技术在金融行业的应用与实践|直播报名预约如何Stand Out竟是伪命题?Yale SOM招生官谈申请的最佳实用建议SpringBoot 22 条最佳实践最佳实践|上海64家商场示范推出“纯净版”停车码实战总结|系统日志规范及最佳实践肥尾效应:应对随机性的最佳实践中国连锁经营协会:2023CCFA零售业供应链最佳实践案例集闲聊高质量发展在申城·杨浦区丨打造人民城市重要理念最佳实践地,推进创新发展再出发MySQL备份恢复最佳实践:终极指南Deadly School Stampede Renews Calls For Scheduling Reform2023金字招牌最佳实践典范新计算范式下,Databricks、Snowflake、Doris、字节跳动的数据平台落地实践|QCon分布式锁实现原理与最佳实践【邀请函】IEEE中国作者研讨会——来自IEEE编委的发文建议及最佳实践面试官:如何使用 Dockerfile 去构建自定义的 Docker 镜像?问倒一大片。。。面试官希望你这样来面试:最佳实践经验[家居][木工]Modified Split Top Roubo Style Mobile Workbench 自制可移动木工桌
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。