压缩列表、字典、跳跃表

数组

我们知道数组在内存中是一段连续的空间,很好的利用CPU缓存访问数据,所以在存储上拥有一定优势。
但是数组要求每个元素的大小相同,如果我们要存储不同长度的字符串,那我们就需要用最大长度的字符串大小作为元素的大小(假设是20个字节)。存储小于 20 个字节长度的字符串的时候,便会浪费部分存储空间。

于是便提出一种想法,给每个节点增加一个length的属性,这样在规定的时候。就可以加入不同大小的对象,并且不会浪费空间。
这种结构就像一个简单的压缩列表了。

Redis压缩列表

压缩列表(zip1ist)是列表和哈希的底层实现之一。 压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构

压缩列表的结构如下图所示:
在这里插入图片描述

属性类型长度说明
zlbytesuint32_t4字节压缩列表占用的内存字节数;
zltailuint32_t4字节压缩列表表尾节点距离列表起始地址的偏移量(单位字节);
zllenuint16_t2字节压缩列表包含的节点数量,等于UINT16_MAX时,需遍历列表计算真实数量;
entryX列表节点不固定压缩列表包含的节点,节点的长度由节点所保存的内容决定;
zlenduint8_t1字节压缩列表的结尾标识,是一个固定值0xFF;

压缩列表的节点由以下字段构成:
在这里插入图片描述
previous_entry_length(pel)属性以字节为单位,记录当前节点的前一节点的长度,其自身占据1字节或5字节:

  1. 如果前一节点的长度小于254字节,则“pel”属性的长度为1字节,前一节点的长度就保存在这一个字节内;
  2. 如果前一节点的长度达到254字节,则“pel”属性的长度为5字节,其中第一个字节被设置为0xFE,之后的四个字节用来保存前一节点的长度;

节点的encoding属性记录了节点的content属性所保存数据的类型以及长度:

encoding长度content
00 xxxxxx1字节最大长度为26 -1的字节数组;
01 xxxxxx bbbbbbbb2字节最大长度为214-1的字节数组;
10 __ bbbbbbbb … … …5字节最大长度为232-1的字节数组;
11 0000001字节int16_t类型的整数;
11 0100001字节int32_t类型的整数;
11 1000001字节int64_t类型的整数;
11 1100001字节24位有符号整数;
11 1111101字节8位有符号整数;
11 11xxxx1字节没有content属性,xxxx直接存[0,12]范围的整数值;

添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率并不高。

字典

typedef struct dict{
         //类型特定函数
         void *type;
         //私有数据
         void *privdata;
         //哈希表-见2.1.2
         dictht ht[2];
         //rehash 索引 当rehash不在进行时 值为-1
         int trehashidx; 
}dict;

字典(dict)又称为散列表,是一种用来存储键值对的数据结构。Redis字典的实现主要涉及三个结构体:字典、哈希表、哈希表节点。其中,每个哈希表节点保存一个键值对,每个哈希表由多个哈希表节点构成,而字典则是对哈希表的进一步封装。

Redis如何解决散列冲突

  1. 链表法
  2. Redis rehash

dict里包含2个dictht,多出的哈希表用于REHASH。当哈希表保存的键值对数量过多或过少时,需要对哈希表的大小进行扩展或收缩操作,在Redis中,扩展和收缩哈希表是通过REHASH实现的,执行REHASH的大致步骤如下:

  1. 为字典的ht[1]哈希表分配内存空间
    如果执行的是扩展操作,则ht[1]的大小为第1个大于等于ht[0].used*2的2n。如果执行的是收缩操作,则ht[1]的大小为第1个大于等于ht[0].used的2n。
  2. 将存储在ht[0]中的数据迁移到ht[1]上
    重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
  3. 将字典的ht[1]哈希表晋升为默认哈希表
    迁移完成后,清空ht[0],再交换ht[0]和ht[1]的值,为下一次REHASH做准备。

为了避免REHASH对服务器性能造成影响,REHASH操作不是一次性地完成的,而是分多次、渐进式地完成的。

渐进式REHASH的详细过程如下:

  1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表;
  2. 在字典中的索引计数器rehashidx设置为0,表示REHASH操作正式开始;
  3. 在REHASH期间,每次对字典执行添加、删除、修改、查找操作时,程序除了执行指定的操作外,还会顺带将ht[0]中位于rehashidx上的所有键值对迁移到ht[1]中,再将rehashidx的值加1;
  4. 随着字典不断被访问,最终在某个时刻,ht[0]上的所有键值对都被迁移到ht[1]上,此时程序将rehashidx属性值设置为-1,标识REHASH操作完成。

REHSH期间,字典同时持有两个哈希表,此时的访问将按照如下原则处理:

  1. 新添加的键值对,一律被保存到ht[1]中;
  2. 删除、修改、查找等其他操作,会在两个哈希表上进行,即程序先尝试去ht[0]中访问要操作的数据,若不存在则到ht[1]中访问,再对访问到的数据做相应的处理。

跳跃表

链表是一个查找效率低的数据结构,于是为了提高效率,可以考虑在链表上建索引的方式。
在这里插入图片描述
这个时候,我们假设要查找节点8,我们可以先在索引层遍历,当遍历到索引层中值为 7 的结点时,发现下一个节点是9,那么要查找的节点8肯定就在这两个节点之间。我们下降到链表层继续遍历就找到了8这个节点。原先我们在单链表中找到8这个节点要遍历8个节点,而现在有了一级索引后只需要遍历五个节点。

如果这时再加一层索引,又会提高它的效率,像这种链表加多层索引的就是跳跃表。

Redis跳跃表

跳跃表的实现主要涉及2个结构体:zskiplist、zskiplistNode。
在这里插入图片描述
其中,蓝色的表格代表zskiplist,红色的表格代表zskiplistNode。

  • header:
    指向跳跃表的表头节点,通过这个指针程序定位表头节点的时间复杂度就为O(1)

  • tail:
    指向跳跃表的表尾节点,通过这个指针程序定位表尾节点的时间复杂度就为O(1)

  • level:
    记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内),通过这个属性可以再O(1)的时间复杂度内获取层高最好的节点的层数。

  • length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内),通过这个属性,程序可以再O(1)的时间复杂度内返回跳跃表的长度。

  • 层(level):

    节点中用1、2、L3等字样标记节点的各个层,L1代表第一层,L代表第二层,以此类推。

    每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离(跨度越大、距离越远)。在上图中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。

    每次创建一个新跳跃表节点的时候,程序都根据幂次定律(powerlaw,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。

  • 后退(backward)指针:

    节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。与前进指针所不同的是每个节点只有一个后退指针,因此每次只能后退一个节点。

  • 分值(score):

    各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。

  • 成员对象(ele):

    各个节点中的ele是节点所保存的成员对象。在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向)。

参考文献
Redis数据结构

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值