redis 自定义数据类型

虽然我们有了 5 种基本类型和 3 种扩展数据类型,但是有些场景下,我们对数据类型会有特殊需求,例如,我们需要一个数据类型既能像 Hash 那样支持快速的单键查询,又能像Sorted Set 那样支持范围查询,此时,我们之前学习的这些数据类型就无法满足需求了。
那么,接下来,我就再向你介绍下 Redis 扩展数据类型的终极版——自定义的数据类型。这样,你就可以定制符合自己需求的数据类型了,不管你的应用场景怎么变化,你都不用担心没有合适的数据类型。

RedisObject

为了实现自定义数据类型,首先,我们需要了解 Redis 的基本对象结构 RedisObject,因为 Redis 键值对中的每一个值都是用 RedisObject 保存的。 RedisObject 包括元数据和指针。
其中,元数据的一个功能就是用来区分不同的数据类型,指针用来指向具体的数据类型的值。所以,要想开发新数据类型,我们就先来了解下 RedisObject 的元数据和指针。

Redis 的基本对象结构

RedisObject 的内部组成包括了 type,、encoding,、lru 和 refcount 4 个元数据,以及 1个*ptr指针。
  • type:表示值的类型,涵盖了我们前面学习的五大基本类型;
  • encoding:是值的编码方式,用来表示 Redis 中实现各个基本类型的底层数据结构,例如 SDS、压缩列表、哈希表、跳表等;
  • lru:记录了这个对象最后一次被访问的时间,用于淘汰过期的键值对;
  • refcount:记录了对象的引用计数;
  • *ptr:是指向数据的指针。
notion image
RedisObject 结构借助*ptr指针,就可以指向不同的数据类型,例如,*ptr指向一个SDS 或一个跳表,就表示键值对中的值是 String 类型或 Sorted Set 类型。所以,我们在定义了新的数据类型后,也只要在 RedisObject 中设置好新类型的 type 和 encoding,再用*ptr指向新类型的实现,就行了。

开发一个新的数据类型

了解了 RedisObject 结构后,定义一个新的数据类型也就不难了。首先,我们需要为新数据类型定义好它的底层结构、type 和 encoding 属性值,然后再实现新数据类型的创建、释放函数和基本命令。 接下来,我以开发一个名字叫作 NewTypeObject 的新数据类型为例,来解释下具体的 4个操作步骤。
notion image

第一步:定义新数据类型的底层结构

我们用 newtype.h 文件来保存这个新类型的定义,具体定义的代码如下所示:
struct NewTypeObject { struct NewTypeNode *head; size_t len; }NewTypeObject;
其中,NewTypeNode 结构就是我们自定义的新类型的底层结构。我们为底层结构设计两个成员变量:一个是 Long 类型的 value 值,用来保存实际数据;一个是*next指针,指向下一个 NewTypeNode 结构。
struct NewTypeNode { long value; struct NewTypeNode *next; };
从代码中可以看到,NewTypeObject 类型的底层结构其实就是一个 Long 类型的单向链表。当然,你还可以根据自己的需求,把 NewTypeObject 的底层结构定义为其他类型。 例如,如果我们想要 NewTypeObject 的查询效率比链表高,就可以把它的底层结构设计成一颗 B+ 树。

第二步:在 RedisObject 的 type 属性中,增加这个新类型的定义

这个定义是在 Redis 的 server.h 文件中。比如,我们增加一个叫作 OBJ_NEWTYPE 的宏定义,用来在代码中指代 NewTypeObject 这个新类型。
#define OBJ_STRING 0 /* String object. */ #define OBJ_LIST 1 /* List object. */ #define OBJ_SET 2 /* Set object. */ #define OBJ_ZSET 3 /* Sorted set object. */ … #define OBJ_NEWTYPE 7

第三步:开发新类型的创建和释放函数

Redis 把数据类型的创建和释放函数都定义在了 object.c 文件中。所以,我们可以在这个文件中增加 NewTypeObject 的创建函数 createNewTypeObject,如下所示:
robj *createNewTypeObject(void){ NewTypeObject *h = newtypeNew(); robj *o = createObject(OBJ_NEWTYPE,h); return o; }
createNewTypeObject 分别调用了 newtypeNew 和 createObject 两个函数,我分别来介绍下。
先说 newtypeNew 函数。它是用来为新数据类型初始化内存结构的。这个初始化过程主要是用 zmalloc 做底层结构分配空间,以便写入数据。
NewTypeObject *newtypeNew(void){ NewTypeObject *n = zmalloc(sizeof(*n)); n->head = NULL; n->len = 0; return n; }
newtypeNew 函数涉及到新数据类型的具体创建,而 Redis 默认会为每个数据类型定义一个单独文件,实现这个类型的创建和命令操作,例如,t_string.c 和 t_list.c 分别对应String 和 List 类型。按照 Redis 的惯例,我们就把 newtypeNew 函数定义在名为t_newtype.c 的文件中。
createObject 是 Redis 本身提供的 RedisObject 创建函数,它的参数是数据类型的 type和指向数据类型实现的指针*ptr。
我们给 createObject 函数中传入了两个参数,分别是新类型的 type 值 OBJ_NEWTYPE,以及指向一个初始化过的 NewTypeObjec 的指针。这样一来,创建的 RedisObject 就能指向我们自定义的新数据类型了。
robj *createObject(int type, void *ptr) { robj *o = zmalloc(sizeof(*o)); o->type = type; o->ptr = ptr; ... return o; }
对于释放函数来说,它是创建函数的反过程,是用 zfree 命令把新结构的内存空间释放掉。

第四步:开发新类型的命令操作

简单来说,增加相应的命令操作的过程可以分成三小步:
  1. 在 t_newtype.c 文件中增加命令操作的实现。比如说,我们定义 ntinsertCommand 函数,由它实现对 NewTypeObject 单向链表的插入操作:
    1. void ntinsertCommand(client *c){ //基于客户端传递的参数,实现在NewTypeObject链表头插入元素 }
  1. 在 server.h 文件中,声明我们已经实现的命令,以便在 server.c 文件引用这个命令,例如:
    1. void ntinsertCommand(client *c)
  1. 在 server.c 文件中的 redisCommandTable 里面,把新增命令和实现函数关联起来。例如,新增的 ntinsert 命令由 ntinsertCommand 函数实现,我们就可以用 ntinsert 命令给 NewTypeObject 数据类型插入元素了。
    1. struct redisCommand redisCommandTable[] = { ... {"ntinsert",ntinsertCommand,2,"m",...} }
      此时,我们就完成了一个自定义的 NewTypeObject 数据类型,可以实现基本的命令操作了。当然,如果你还希望新的数据类型能被持久化保存,我们还需要在 Redis 的 RDB 和AOF 模块中增加对新数据类型进行持久化保存的代码,