在Redis modules的实现自己的数据类型

注册类型

  Redis modules 能够支持使用高级API(RedisModule_Call)或者 低级API 去直接访问操作 Redis 内置的数据结构。通过使用这些能力我们能够在已存的Redis数据结构上建立一个新的抽象,或者使用 string DMA来把一个Redis module 的数据结构编码成 Redis的string,有了这些方法,我们就有可能使用Redis Module在Redis 内核中创建了一个新数据结构。然而,还有一些更多需要思考的点,不像前面讲的那么简单,接下来会详细介绍。

概况注册数据类型的步骤

  注册一个数据类型主要由以下几步构成
1. 需要某种新的数据类型与其配套操作它的命令的实现
2. 一系列回调函数比如: RDB saving , RDB loading , AOF rewriting , release value (与该key相关的value),相应的hash函数实现(需要实现一个用来hash该数据类型的函数,当Redis使用 DEBUG DIGEST 命令的时候会将所有的key-value 进行hash)。
3. 注册的类型名是一个在所有模块中都独一无二的名字。注意这个类型名长9个字符,由A-Z 、a-z、0-9、_ 、– 构成,如果名字不是恰好9个字符注册失败。
4. 一个编码version,它用来被持久化到RDB文件中标识一个module相应的数据version号。
  当我们第一次看到这里的时候觉得要处理RDB load、save 和 AOF rewrite 感觉很麻烦,但是Redis 提供了非常高级的API供我们使用,我们不必去担心 I/O细节,实际上实现一个 新的数据类型在Redis中是一个新简单的任务
  在Redis源码中的/modules/hellotype.c 文件中有很好的列子,它很通俗易懂,大家可以去看看

注册一个数据类型

  为了在Redis内核中注册一个数据类型,我们需要在我们实现的module中去定义一个全局变量,用它去保存我们注册的数据类型。RedisModule_CreateDataType 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在注册一个新的数据类型的时候是必须要被调用的。在注册的时候,我们给这个API传递了大量的有关相应命令的函数指针作为参数(tm参数)。上面tm结构体里的函数指针是必须传的,然而除了 .digest 与 .mem_usage 这俩个是可选的,并且目前大多数情况下是不支持modules自己去实现它,所以我们可以忽略它们俩个。

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

  上面的encver参数是一个 编码的version号,当模块自定义注册的数据被存进RDB文件的时候需要使用它。value参数是一个我们要注册的自定义类型的一个结构体,它会被传递给每个需要实现的函数中,即如上所述的rdb_load、rdb_save、aof_rewrite、digest、free、mem_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 参数 key的内容被重新创建的时候的命令序列。
  • digest 当DEBUG DIGSET 指令被执行的时候Redis发现了自定义类型的key的时候会被调用。目前它是没有实现的,所以这个函数基本上是不设置。DEBUG DIGSET 命令是用来对整体value一个hash的,所以这个命令的实现基本上我们只需调用相应API把我们自定义类型的每一个value都追加到 Digest 结构中即可。它最后会自己hash
  • mem_usage 当MEMORY 指令调用的时候这个函数会被调用,这个函数被用来统计所有的module value 占用的字节数。
  • free 当一个自定义类型的key 通过 DEL指令被删除的时候它会被用,这个为了让模块能够释放相应value的内存
set and get key

  当我们在RedisModule_OnLoad函数中注册了自己的数据类型后,我们也需要去能够设置我们注册的key。
  我们可以去使用相应的API去 test key (看这个key是否存在)、set key与get key。我们使用RedisModule_OpenKey去访问一个key,然后调相应的 Set 、Get Value函数去操作这个key。

RedisModule_ModuleTypeSetValue
RedisModule_ModuleTypeSetValue(RedisModuleKey * key, RedisModuleType * type ,void * value)

  这个函数有三个参数。第一个参数是以WRITE打开的key、MyType是一个自定义类型的指针在我们注册类型的时候也就是CreateDataType的时候返回的,最后这个void * 的指针是一个自定义类型的结构体。下面是一个set 一个自定义数据类型的列子

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

  注意Redis 不会知道你存了什么数据在value中。它仅仅只会在你注册这个类型的相应操作的时候调用这个数据类型的回调函数来操作它。

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

  上面这个列子是用来去查看对应key的value

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

  为了正确操作key,我们需要去检测这个key是否为空,看是否这个key存在相应的值。以下是个常用的代码用来实现一个向我们自定义数据类型写数据的写命令

int RedisModule_KeyType(key) 这个API常常用来查看是否为空KEY在自定义数据结构中,详情请看 4.0 模块扩展博客
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);
}

  如果我们成功地检测了一个key类型的合法性,那么我们就能去向它写数据。通常上我们想创建一个key或者查找相应key对应的value,可以看下面的代码式列

/* 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'... */
Free method

  如上所提及的,当Redis需要去释放我们自定义类型的value时候,它需要从模块中得到帮助为了能够成功释放对应的内存。这就是为什么我们必须在注册的时候去设置free 函数指针

typedef void (*RedisModuleTypeFreeFunc)(void *value);
RDB load and save methods

  RDB save 和 load 回调函数当Redis在创建(加载)数据的副本在硬盘上的时候会被调用。Redis 提供了一个高级API能够自动保存数据到RDB文件,如下支持类型:
- Unsigned 64 bit integers
- Signed 64 bit integers
- Doubles
- Strings
  如何使用上面的类型取决于模块本身,然而注意 int 和 double的值是以未知的字节序和 浮点数体系存储或者加载的。如果你想使用string 类型的save API你自己必须要注意那些细节。

RDB save 和 load API
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);

  这些Save函数返回值都是void,所以我们写的时候不需要检查它们返回值,Redis认为这些函数的调用总是成功的。举个使用上面接口的列子,假设我们注册了一个类型,这个类型是double_array类型的一个数组

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

  我们的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]);
}

  rdb_load函数代码如下

void *DoubleArrayRDBLoad(RedisModuleIO *io, int encver) {
    if (encver != DOUBLE_ARRAY_ENC_VER) {
        //在这里我们有俩个必须做的事情,第一个是输出LOG第二个是处理旧的版本号
        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 = RedisModule_LoadDouble(io);
    return da;
}

  这个 load函数仅仅用来从RDB文件中重构数据。注意虽然处理I/O的API没有错误处理,但仍然load callback函数可以返回NULL来表示它没有读取到正确的数据。当返回NULL的时候Redis会引起注意。

AOF 重写
void RedisModule_EmitAOF(RedisModuleIO *io, const char *cmdname, const char *fmt, ...);
处理多个编码

WORK IN PROGRESS

申请内存

  Modules 数据类型应该多尝试使用 RedisModule_Alloc 的家族函数去 malloc、realloc、release 堆的内存
  虽然使用RedisModule_Alloc 家族函数的API不会帮助Redis去统计该 module使用的内存,但是它还是有以下优点的

  • Redis 使用jemalloc 去管理堆上内存,jemalloc 通常会避免内存锁片问题(libc 的malloc可能会造成内存锁片问题)
  • 当使用load_string API从RDB文件读取string 数据的时候,它返回的string 直接使用RedisModule_Alloc函数申请的,所以自定义类型的 MyTypeRDBLoad 可以直接返回这个string 不需要再调其他API去拷贝这个结果了。
      如果你已经使用了libc的API ,可以使用下面的宏方便替换
#define malloc RedisModule_Alloc
#define realloc RedisModule_Realloc
#define free RedisModule_Free
#define strdup RedisModule_Strdup

  需要注意的是 混合使用会造成崩溃和一些bug。如果你使用了宏进行替换,你就得所有的调用都是正确的,杜绝 像 RedisModule_Free 一个 malloc 申请的指针这种代码的出现

猜你喜欢

转载自blog.csdn.net/sdoyuxuan/article/details/82501680
今日推荐