REDIS 数据结构与对象

2,172 阅读10分钟

文章顶部.png

云家.png

前言

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

未命名文件.jpg

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

redisObject.jpg

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 结构与数据结构,如下图所示:

image-20211208160405652.png

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

image-20211213193147719.png

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

2、列表对象

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

2.1 zipList 编码

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

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

image-20211208195738936.png

示例:

image-20211208200056596.png

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

2.2 linkedlist 编码

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

image-20211214150539972.png

图1.7 linkedlist 对象结构

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

image-20230731191054901.png

图8 双端链表对象结构

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

3、哈希对象

3.1 zipList 编码

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

image-20230731191902663.png

​​

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

3.2 hashtable 编码

image-20230731191610657.png

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 值链接起来,如下图所示:

image-20230731192940395.png

hash表节点解决hash冲突

3.2 rehash 操作

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

image-20230731194313708.png

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

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

image-20230731201316543.png

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

image-20230731194210061.png

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

image-20230731194427851.png

4、集合对象

4.1 Intset

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

image-20230731200248854.png

4.2 hashTable

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

image-20230731195751792.png

5、有序集合对象

5.1 ziplist

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

image-20230731200800769.png

5.2 skiplist

image-20230731204435786.png

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设计与实现(第二版)》

推荐阅读

Lombok技术揭秘 _ 自动生成带代码的幕后机制

构建RFM体系:优化客户分析和营销策略

基于jvm-sandbox-repeater的流量降噪方案

海量数据处理方案

工作了5年你居然不知道版本号有这些规范?

招贤纳士

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

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

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注 文章顶部.png