redis数据类型及底层实现(二)

Hash数据结构介绍:

案例:
hash-max-ziplist-entries:使用压缩列表保存时哈希集合中的最大元素个数。
hash-max-ziplist-value:使用压缩列表保存时哈希集合中单个元素的最大长度。

Hash类型键的字段个数 小于 hash-max-ziplist-entries 并且每个字段名和字段值的长度 小于 hash-max-ziplist-value 时,
Redis才会使用 OBJ_ENCODING_ZIPLIST来存储该键,前述条件任意一个不满足则会转换为 OBJ_ENCODING_HT的编码方式
在这里插入图片描述
在这里插入图片描述
结构:
hash-max-ziplist-entries:使用压缩列表保存时哈希集合中的最大元素个数。
hash-max-ziplist-value:使用压缩列表保存时哈希集合中单个元素的最大长度。

结论:
1.哈希对象保存的键值对数量小于512个
2.所有的键值对的键和值的字符串长度都小于等于64byte(一个英文字母一个字节)
3.ziplist升级到hashtable可以,反过来降级不可以
一旦从压缩列表转为了哈希表,Hash类型就会一直用哈希表进行保存而不会再转回压缩列表了。
在节省内存空间方面哈希表就没有压缩列表高效了。
流程:
在这里插入图片描述
hash的两种编码格式:
1.ziplist
2.hashtable

源码分析:
ziplist.c

Ziplist 压缩列表是一种紧凑编码格式,总体思想是多花时间来换取节约空间,即以部分读写性能为代价,来换取极高的内存空间利用率,
因此只会用于 字段个数少,且字段值也较小 的场景。压缩列表内存利用率极高的原因与其连续内存的特性是分不开的。
想想我们的学过的一种GC垃圾回收机制:标记–压缩算法
当一个 hash对象 只包含少量键值对且每个键值对的键和值要么就是小整数要么就是长度比较短的字符串,那么它用 ziplist 作为底层实现

ziplist什么样:

ziplist是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,节约内存,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
ziplist各个组成单元什么意思:

在这里插入图片描述
明明有链表了,为什么出来一个压缩链表?

1 普通的双向链表会有两个指针,在存储数据很小的情况下,**我们存储的实际数据的大小可能还没有指针占用的内存大,得不偿失。**ziplist 是一个特殊的双向链表没有维护双向指针:prev next;而是存储上一个 entry的长度和 当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度更费内存。这是典型的“时间换空间”。

2 链表在内存中一般是不连续的,遍历相对比较慢,而ziplist可以很好的解决这个问题,普通数组的遍历是根据数组里存储的数据类型找到下一个元素的(例如int类型的数组访问下一个元素时每次只需要移动一个sizeof(int)就行),但是ziplist的每个节点的长度是可以不一样的,而我们面对不同长度的节点又不可能直接sizeof(entry),所以ziplist只好将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点。

3 头节点里有头节点里同时还有一个参数 len,和string类型提到的 SDS 类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到len值就可以了,这个时间复杂度是 O(1)

在这里插入图片描述
压缩列表节点的构成:
压缩列表是 Redis 为节约空间而实现的一系列特殊编码的连续内存块组成的顺序型数据结构,本质上是字节数组
在模型上将这些连续的数组分为3大部分,分别是header+entry集合+end,
其中headerzlbytes+zltail+zllen组成,
entry是节点,
zlend是一个单字节255(1111 1111),用做ZipList的结尾标识符。见下: 压缩列表结构:由zlbytes、zltail、zllen、entry、zlend这五部分组成

在这里插入图片描述
zlbytes 4字节,记录整个压缩列表占用的内存字节数。
zltail 4字节,记录压缩列表表尾节点的位置。
zllen 2字节,记录压缩列表节点个数。
zlentry 列表节点,长度不定,由内容决定。
zlend 1字节,0xFF 标记压缩的结束。

zlentry实体结构解析:
官网源码
在这里插入图片描述
解析:
压缩列表zlentry节点结构:每个zlentry由前一个节点的长度、encoding和entry-data三部分组成

在这里插入图片描述
前节点:(前节点占用的内存字节数)表示前1个zlentry的长度,prev_len有两种取值情况:1字节或5字节。取值1字节时,表示上一个entry的长度小于254字节。虽然1字节的值能表示的数值范围是0到255,但是压缩列表中zlend的取值默认是255,因此,就默认用255表示整个压缩列表的结束,其他表示长度的地方就不能再用255这个值了。所以,当上一个entry长度小于254字节时,prev_len取值为1字节,否则,就取值为5字节。
**enncoding:**记录节点的content保存数据的类型和长度。
**content:**保存实际数据内容
在这里插入图片描述

typedef struct zlentry {    // 压缩列表节点
    // prevrawlen是前一个节点的长度
   unsigned int prevrawlensize, prevrawlen;  
   //prevrawlensize是指prevrawlen的大小,有1字节和5字节两种
    unsigned int prevrawlensize, prevrawlen;    
    // len为当前节点长度 lensize为编码len所需的字节大小
    unsigned int lensize, len;  
     // 当前节点的header大小
    unsigned int headersize;   
    // 节点的编码方式
    unsigned char encoding; 
    // 指向节点的指针
    unsigned char *p;   
} zlentry;

压缩列表的遍历:
通过指向表尾节点的位置指针p1, 减去节点的previous_entry_length,得到前一个节点起始地址的指针。如此循环,从表尾遍历到表头节点。从表尾向表头遍历操作就是使用这一原理实现的,只要我们拥有了一个指向某个节点起始地址的指针,那么通过这个指针以及这个节点的previous_entry_length属性程序就可以一直向前一个节点回溯,最终到达压缩列表的表头节点。

ziplist存取情况:
在这里插入图片描述
在这里插入图片描述

t_hash.c:
在这里插入图片描述
在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构

OBJ_ENCODING_HT 编码分析:
在这里插入图片描述
每个键值对都会有一个dictEntry

List数据结构介绍:

案例:
在这里插入图片描述
(1) ziplist压缩配置:list-compress-depth 0
表示一个quicklist两端不被压缩的节点个数。这里的节点是指quicklist双向链表的节点,而不是指ziplist里面的数据项个数
参数list-compress-depth的取值含义如下:
0: 是个特殊值,表示都不压缩。这是Redis的默认值。
1: 表示quicklist两端各有1个节点不压缩,中间的节点压缩。
2: 表示quicklist两端各有2个节点不压缩,中间的节点压缩。
3: 表示quicklist两端各有3个节点不压缩,中间的节点压缩。
依此类推…

(2) ziplist中entry配置:list-max-ziplist-size -2
当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度。比如,当这个参数配置成5的时候,表示每个quicklist节点的ziplist最多包含5个数据项。当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度。这时,它只能取-1到-5这五个值,
每个值含义如下:
-5: 每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
-4: 每个quicklist节点上的ziplist大小不能超过32 Kb。
-3: 每个quicklist节点上的ziplist大小不能超过16 Kb。
-2: 每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值)
-1: 每个quicklist节点上的ziplist大小不能超过4 Kb。

List的一种编码格式:
list用quicklist来存储,quicklist存储了一个双向链表,每个节点都是一个ziplist
在这里插入图片描述
在低版本的Redis中,list采用的底层数据结构是ziplist+linkedList;

高版本的Redis中底层数据结构是quicklist(它替换了ziplist+linkedList),而quicklist也用到了ziplist

quicklist :
在低版本的Redis中,list采用的底层数据结构是ziplist+linkedList;
高版本的Redis中底层数据结构是quicklist(它替换了ziplist+linkedList),而quicklist也用到了ziplist。

quicklist 实际上是 zipList 和 linkedList 的混合体,它将 linkedList按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。
在这里插入图片描述
是ziplist和linkedlist的结合体

源码分析:
quicklist.h,head和tail指向双向列表的表头和表尾
在这里插入图片描述
在这里插入图片描述
quicklistNode中的*zl指向一个ziplist,
一个ziplist可以存放多个元素:
在这里插入图片描述
在这里插入图片描述

Set数据结构介绍:

案例:
Redis用intset或hashtable存储set。如果元素都是整数类型,就用intset存储。
如果不是整数类型,就用hashtable(数组+链表的存来储结构)。key就是元素的值,value为null。

在这里插入图片描述
Set的两种编码格式:
intset
hashtable

源码分析:
t_set.c
在这里插入图片描述
在这里插入图片描述

ZSet数据结构介绍:

案例:
当有序集合中包含的元素数量超过服务器属性 server.zset_max_ziplist_entries 的值(默认值为 128 ),
或者有序集合中新添加元素的 member 的长度大于服务器属性 server.zset_max_ziplist_value 的值(默认值为 64 )时,
redis会使用跳跃表作为有序集合的底层实现。
否则会使用ziplist作为有序集合的底层实现
在这里插入图片描述
在这里插入图片描述
ZSet的两种编码格式:
1.ziplist
2.skiplist

源码分析;
t_zset.c
在这里插入图片描述
在这里插入图片描述

skiplist跳表:

是什么?
跳表是可以实现二分查找的有序链表
skiplist是一种以空间换取时间的结构。

由于链表,无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找。

提取多层关键节点,就形成了跳跃表
总结来讲 跳表 = 链表 + 多级索引

说说链表和数组的优缺点?为什么引出跳表

痛点:
在这里插入图片描述
解决方法:升维,也叫空间换时间。

优化:
在这里插入图片描述

跳表的时间复杂度:

跳表查询的时间复杂度分析

首先每一级索引我们提升了2倍的跨度,那就是减少了2倍的步数,所以是n/2、n/4、n/8以此类推;
第 k 级索引结点的个数就是 n/(2^k);
假设索引有 h 级, 最高的索引有2个结点;n/(2^h) = 2, 从这个公式我们可以求得 h = log2(N)-1;
所以最后得出跳表的时间复杂度是O(logN)
在这里插入图片描述
时间复杂度是O(logN)

跳表的空间复杂度:
跳表查询的空间复杂度分析

首先原始链表长度为n
如果索引是每2个结点有一个索引结点,每层索引的结点数:n/2, n/4, n/8 … , 8, 4, 2 以此类推;
或者所以是每3个结点有一个索引结点,每层索引的结点数:n/3, n/9, n/27 … , 9, 3, 1 以此类推;
所以空间复杂度是O(n);

所以空间复杂度是O(N)

优缺点:

跳表是一个最典型的空间换时间解决方案,而且只有在数据量较大的情况下才能体现出来优势。而且应该是读多写少的情况下才能使用,所以它的适用范围应该还是比较有限的

维护成本相对要高 - 新增或者删除时需要把所有索引都更新一遍;
最后在新增和删除的过程中的更新,时间复杂度也是O(log n)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值