Redis 的底层数据结构(对象)

目前为止,我们介绍了 redis 中非常典型的五种数据结构,从 SDS 到 压缩列表,这都是 redis 最底层、最常用的数据结构,相信你也掌握的不错。

但 redis 实际存储键值对的时候,是基于对象这个基本单位的,并且往往一个对象下面对对应不同的底层数据结构实现以便于在不同的场景下切换底层实现提升效率。例如列表对象在元素不多情况话会使用压缩列表来实现以压缩内存,而在元素比较多的时候常规的双端链表进行实现。

下面我们就具体来看看 redis 中都有哪些对象,底层又对应哪些可供选择的数据结构。

一、Redis 对象结构定义

redis 为每个对象定义为如下数据结构:

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; 
    int refcount;
    void *ptr;
} robj;

type 记录的是当前的对象类型,有以下几种类型:

#define OBJ_STRING 0   /*字符串对象*/

#define OBJ_LIST 1    /*列表对象*/

#define OBJ_SET 2    /*集合对象*/

#define OBJ_ZSET 3   /*有序集合对象*/

#define OBJ_HASH 4   /*哈希对象*/

encoding 记录的是当前对象使用的哪种底层数据结构实现的,有以下类型可供选择:

#define OBJ_ENCODING_RAW 0     /* SDS 字符串 */

#define OBJ_ENCODING_INT 1     /* 整数 */

#define OBJ_ENCODING_HT 2      /* 字典结构 */

#define OBJ_ENCODING_ZIPMAP 3  /* 压缩map,已经废弃 */

#define OBJ_ENCODING_LINKEDLIST 4 /* LinkedList 双端链表,废弃了 */

#define OBJ_ENCODING_ZIPLIST 5 /* 压缩列表 */

#define OBJ_ENCODING_INTSET 6  /* 整数集合 */

#define OBJ_ENCODING_SKIPLIST 7  /* 跳跃表 */

#define OBJ_ENCODING_EMBSTR 8  /* 短字符串 */

#define OBJ_ENCODING_QUICKLIST 9 /* 压缩链表和双向链表组成的快速列表 */

8 和 9 我们遇到时在介绍,这里暂时不做介绍。

lru 记录的是上一个当前对象实例被访问的时间,它用作计算对象空转时长,空转时长过大的对象会被 redis 优先释放内存。

refcount 记录的是对象的引用计数,引用计数算法是很多编程语言中管理对象是否应该被销毁的依据,和它类似的典型的 Java 中可达性分析算法,都是用于计数当前对象是否依然被使用,以便释放内存。

ptr 指针指向的是实际实现当前对象的数据结构首地址。

以上就是 redisObject 数据结构的基本解释,下面我们看具体的对象分别会在什么情况下切换不同的底层实现。

二、字符串对象

字符串对象有三种 encoding 值,也就是只有这三种情况,redis 才会使用字符串对象存储数据。

  1. 字符串(raw):普通的字符串
  2. 整数(int):long 类型的整数值
  3. 短字符串(embstr ):短字符串

如果判定使用 raw 编码,那么 redis 的 ptr 指针将会指向一个 SDS 结构,如果确定使用 int 编码,那么会将 redisObject 中 ptr 类型由 void* 变成 long,继而分配 robj 内存。

当字符串的长度小于 39 个字节时,会采用 embstr 这种编码,embstr 其实也是使用 SDS 进行存储,区别于 raw 编码的是,后者会将 robj 和 ptr 指向的 SDS 分配在连续的内存块,唯一的好处是分配和释放内存都只需要一次操作即可完成,再一个是因为数据相邻,有可能一次加载 robj 的时候,CPU 将后面的 embstr 也加载进缓存,等到访问的时候就可以直接从缓存中访问。

但是,我们看一个例子:

image

hello 原本是以 int 编码存储的,但是我们执行 append 命令添加了字符串之后,它变成了 raw 编码。

这其实是 redis 的一种编码换换,当 hello 不再适合使用 int 编码继续存储的时候,会进行一个编码转换。

三、列表对象

列表对象有两种编码,压缩列表 ziplist 和 linkedlist。我们之前说过压缩列表的推荐应用场景,少量整数或字符串的时候可以用压缩列表来节省内存空间,而大数据量的节点则推荐使用普通的双端链表进行实现。

但是实际上,redis 的较新版本已经使用一种叫 quicklist 的快速列表整合 ziplist 和 linkedlist 作为列表对象的实现了。它将所有的节点分段拆分,每一份又使用压缩列表进行压缩,不同段之间使用双向指针连接。

image

四、集合对象

集合对象也有两种编码,整数集合 intset 和 字典 hashtable。默认情况下,当集合中有且仅有整型数据,且不超过 512 个,那么 redis 会使用整数集合 intset 进行集合存储,其余情况 redis 则构建字典进行集合数据存储。

image

顺便给大家复习下 intset 的无重复性、顺序性的特性,重复的元素是插入不进去的,因为插入之前会通过二分查找查找是否存在该元素,如果存在则拒绝插入操作。

当然了,如果集合中元素个数超过 512,那么 redis 就转而使用字典结构进行数据存储,具体实例就不再演示了。

五、有序集合对象

有序集合对象同样使用两种编码 ziplist 和 skiplist,可能你又见到压缩列表的身影了,足以见得,压缩列表是一个非常优秀的数据结构。

同样,当有序集合中包含少量元素的时候,redis 会优先使用压缩列表进行存储,反之选择跳跃表。

image

sadd 命令的标准语法是:

ZADD KEY_NAME SCORE1 VALUE1.. SCOREN VALUEN

每一个元素都会对应一个分值,skiplist 本身的实现就需要这个分值进行元素的存储排序,有的时候有序集合会使用压缩列表进行实现,那么也需要这个分值来有序的压缩元素,这也是压缩列表页可以实现有序集合的原因。

这里补充一下,虽然说 redis 的有序集合是跳表实现的,这句话不错,但有失偏驳。

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

准确来说,redis 中的有序集合是由我们之前介绍过的字典加上跳表(组合起来就是zset)实现的,字典中保存的数据和分数 score 的映射关系,每次插入数据会从字典中查询,如果已经存在了,就不再插入,有序集合中是不允许重复数据。

六、哈希对象

哈希对象的编码可以是 ziplist 或者 hashtable,没什么特殊的,不再赘述。

image

以上,我们总结了 redis 中五大对象结构,以及他们可选的底层实现数据结构,相信你也理解的不错,这将非常有助于我们后面的学习。

下节开始,我们向 redis 数据库迈进~


关注公众不迷路,一个爱分享的程序员。

公众号回复「1024」加作者微信一起探讨学习!

每篇文章用到的所有案例代码素材都会上传我个人 github

https://github.com/SingleYam/overview_java

欢迎来踩!

YangAM 公众号

posted @ 2019-11-28 21:42  Single_Yam  阅读(1569)  评论(1编辑  收藏  举报