likes
comments
collection
share

REDIS 数据结构与对象

作者站长头像
站长
· 阅读数 11

REDIS 数据结构与对象

REDIS 数据结构与对象

前言

Redis 是一种非关系类型数据库,以(k, v)的形式储存数据信息。由于读写速度很快,常被应用于缓存方向。Redis 使用对象来代表数据库中的键和值。Redis 有 5 种数据对象,分别是字符串对象( String 对象)、列表对象( List )、哈希对象( Hash )、集合对象、以及有序集合对象(Sorted Set)五种。其 key 值的形式都是使用的字符串形式,value 的形式可以是上面五种对象中的任意一种。 Redis 对象内存结构如图1所示:

REDIS 数据结构与对象

这里 type 代表对象的种类。encoding 代表这个对象的编码形式。ptr 代表着指向储存数据的指针,针对不同的数据对象,有着不同的编码形式,其底层的数据结构也是不尽相同的。下图为 Redis 数据对象编码方式与数据结构的对应关系。

REDIS 数据结构与对象

1、字符串对象

Redis 的键对象都是字符串对象。并且值也可以使用字符串对象创建。根据编码形式的不同,分为 int 编码,raw 编码以及 embstr 三种编码形式。

1.1 int编码

当 Redis 的对象中的值储存整数值时,其使用的便是 int 编码。其数据直接储存在内存当中,无需特殊的数据结构储存数据。

1.2 raw编码

Raw 编码底层的数据结构使用的是 SDS(简单动态字符串)结构。Redis 底层是用 c 语言实现的。但是在实现字符串对象的数据结构时,并没有使用简单的字符串的数据结构形式。而是使用 SDS 的结构进行储存。相比较普通字符串,SDS 对象结构如下所示

/*  
 * 保存字符串对象的结构  
 */  
struct sdshdr {  
      
    // buf 中已占用空间的长度  
    int len;  
  
    // buf 中剩余可用空间的长度  
    int free;  
  
    // 数据空间  
    char buf[];  
};

这里 len 用来记录数据空间已经使用的长度,free 表示数据空间剩余的长度。buf 用来表示储存数据的数据空间。由此对象可知,SDS 结构的好处有:

1、获取对象长度的时间负杂度为 O(1)。

2、减少更改字符串时内存的重新分配次数 。SDS 在进行更改操作时,会进行预检查,查看剩余空间是否足够,如不够的话,会进行扩展,然后进行字段的拼接或者其它操作。并且由于含有 len 字段以及 free 字段。在进行内存的重新分配,一般来说。进行 n 次字符串的增长会进行n次的扩容,这里使用 SDS 扩容的次数哦可以小于 n 次。

3、能够保存特殊的字符。普通字符串由于需要判断字符串截止位置,会以第一个出现空字符的位置认为是字符串的截止位置。所以只能用来保存文本数据,这里 SDS 记录的字符串的使用长度 len,因此就算数据中含有空字符,仍然是可以储存的。

1.3 embstr 编码

同 raw 一样,embstr 编码底层储存数据的结构也是 SDS 形式。但是两者储存字符串的长度不同,并且内存的分配策略也有所不同,对于 embstr 使用于储存小于32字节的字符串值,分配一个连续的空间用来储存 redisObject 结构与数据结构,如下图所示:

REDIS 数据结构与对象

而对于 raw 来说,需要进行两次内存分配,创建对象时需要为 redisObject 以及 SDS 分别分配一块儿内存,两者使用 ptr 进行链接

REDIS 数据结构与对象

这里,当 int 编码加入非整数字符或者 embstr 的编码长度增加超过 32 字节时,会转换为 raw 编码。

2、列表对象

列表对象有两种数据储存的结构,其中 zipList 编码的底层数据结构是压缩列表,而 linklist 编码使用饿底层数据结构为链表。

2.1 zipList 编码

使用压缩列表的前提条件:一是所有字符串长度都小于 64 字节,二是元素数量小于 512,这两个条件可以在 Redis 的配置文件中修改, list-max-ziplist-value 选项和 list-max-ziplist-entries 选项。

压缩列表是 Redis 为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型( sequential )数据结构。一个压缩列表可以包含任意多个节点( entry ),每个节点可以保存一个字节数组或者一个整数值,如下图。

REDIS 数据结构与对象

示例:

REDIS 数据结构与对象

这里列表总长为 0x5c(80),初始节点与末尾节点的偏移量为 0x3c(60).当我们知道初始节点的地址 p 时,就可以计算出末尾节点的地址为 p+60。

2.2 linkedlist 编码

当 Redis 需要储存的对象不满足压缩列表所要求的对象时,此时使用 linkedlist 编码形式储存对象,linkedlist 底层使用的是双端链表。每个节点都会储存一个字符串对象,而每个字符串对象中都储存了一个列表元素,如图所示

REDIS 数据结构与对象

图1.7 linkedlist 对象结构

双端列表是由多个链表节点构成,节点含有指向前方的指针 prev 以及指向后方的节点 next

REDIS 数据结构与对象

图8 双端链表对象结构

对外提供节点值的复制,释放以及对比函数来设置类型特定的函数。使用列表对象适用于做简单的消息队列功能;最新消息排行等功能。

3、哈希对象

3.1 zipList 编码

当每一个对象的大小都比较小并且整体的个数不大的情况下,可以使用压缩列表的方式来实现 Redis 的 hash 对象的储存如下图所示:

REDIS 数据结构与对象

​​

整体组成结构和上边列表相似,只不过压缩列表相邻位置以 key-value 形式来储存对象

3.2 hashtable 编码

REDIS 数据结构与对象

hashtable 编码方式底层使用字典的方式进行储存数据,其中 hash 表使用 dict 结构定义

typedef struct dictht {
   //哈希表数组
   dictEntry **table;
   //哈希表大小
   unsigned long size;

   //哈希表大小掩码,用于计算索引值
   unsigned long sizemask;
   //该哈希表已有节点的数量
   unsigned long used;
}

其中 table 指向 dictEntry 数组,数组中储存的 key 以及 value 的值,但是我们存入时并不是简单的将 key 存入对应的位置。这里会通过 hash 算法将字符串转换为对应的 hash 值,然后储存的 dictentry 对应的位置上。这里,如果两个 key 的 hash 值相同时,这里会以链表的形式将两个 key 值链接起来,如下图所示:

REDIS 数据结构与对象

hash表节点解决hash冲突

3.2 rehash 操作

当 hash 表保存的键值对不断增加或者减少,为了让哈希表的负载因子维持在一个合理的范围之内,这时候就需要通过 rehash 操作完成

REDIS 数据结构与对象

这里,当 hash 表里的内容超过负载因子之后,会进行 rehash 操作,对 hash 表进行扩展,扩展规则:

1、ht(1) 的大小是超过 h(0) 大小的第一个 2^n 次幂。就比如现在 h(0) 是 4 时,大于 4 的第一个 2^n 为 8,这时候 h(1) 的大小就会扩容成 8.

REDIS 数据结构与对象

2、此时将 h(0) 的数据转移到 h(1) ,对应位置重新通过 hash 值进行计算

REDIS 数据结构与对象

3、最后将 h(0) 释放,h(1) 设置成为 h(0), 为 h(1) 分配一个空白的 hash 表

REDIS 数据结构与对象

4、集合对象

4.1 Intset

当需要储存的底层储存对象都是整数,并且整体个数小于 512 (此值可以通过修改Redis配置文件中的 et-max-intset-entries 修改)时,此时使用的是 intset 编码,ptr 指向整数集合当中

REDIS 数据结构与对象

4.2 hashTable

集合对象也可以使用 hashtable 的结构进行储存,与前面 hash 对象不同的是,这里只是使用了键来保存集合对象,key 值指向 null.

REDIS 数据结构与对象

5、有序集合对象

5.1 ziplist

当有序集合对象需要储存的数据所有元素长度小于 64 字节,并且元素个数小于 128 个时,使用压缩列表来储存有序集合对象,这里两个相邻的数据分别储存元素值以及对应的分值,分支小的靠近表头,分支大的靠近表尾,这样的储存形式保证了数据的有序性。

REDIS 数据结构与对象

5.2 skiplist

REDIS 数据结构与对象

skiplist 编码方式是将字典的编码方式与跳跃表的方式相结合,字典在上面已经提到了,有点是查询的负责度为 O(1) ,但是保证不了有序,这里就要引入跳跃表来实现集合的顺序性。跳跃表拥有指向头部以及尾部的指针,以及元素个数的 length 和表示最高层高的 level。下面代表一个节点的数据结构

typedef struct zskiplistNode{   
//每一层带有两个属性 前进指针 跨度
  struct zskiplistLevel{     
     //前进指针
    struct zskiplistNode *forward;   
        //跨度
    unsigned int span;
  }   
  level[];  
  //后退指针 BW
  struct zskiplistNode *backward;  
  //分值
  double score;
 //成员对象
  robj *obj;
}

从结构来看,跳表有着前进指针以及后退指针,并且针对前进指针有着对应的跨度。节点对象包含分值,节点按照分支的大小从小向大排序。这里使用两种结构来保存数据,这里分值和成员都是共享的,指针指向统一地址,因此不会占用内存。

总结

Redis 提供了多种数据类型用来支持不同的业务场景,比如微博粉丝的关注列表、排行榜等。不同的数据类型适用于不同的场景。本文讲解了 Redis 的数据对象以及其底层实现的数据结构。感受一下 Redis 对于内存以及查询速度进行的设计以及优化形式。

参考文献

《Redis设计与实现(第二版)》

推荐阅读

招贤纳士

政采云技术团队(Zero),Base 杭州,一个富有激情和技术匠心精神的成长型团队。规模 500 人左右,在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。

如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号