redis的底层数据结构汇总

文章目录

    • 第一部分 数据结构与对象
      • 一、简单动态字符串
        • 1.1 作用:
        • 1.2. 定义:
        • 1.3. 与c语言获取str长度的区别:
        • 1.4. 减少修改字符串时带来的内存重新分配次数:
        • 1.5. 二进制安全:
        • 1.6. 兼容部分c字符串函数:
        • 1.7.小结:
      • 二、链表
        • 2.1. 链表与链表节点的实现
        • 2.2.小结
      • 三、字典
        • 3.1.哈希表
        • 3.2.哈希表节点
        • 3.3.字典
        • 3.4.哈希算法
        • 3.5.解决键冲突
        • 3.6.rehash
        • 3.7.渐进式rehash
        • 3.8.小结
      • 四、跳跃表
        • 4.1.跳跃表的实现
        • 4.2.跳跃表节点
        • 4.3.跳跃表
        • 4.4.小结
      • 五、整数集合
        • 5.1.整数集合的实现
        • 5.2.升级
        • 5.3.升级的的好处
      • 六、压缩列表
        • 6.1.压缩列表的构成
        • 6.2.压缩列表节点的构成
        • 6.3.连锁更新
        • 6.4.小结
      • 七、对象
        • 7.1.1.对象的类型与编码
        • 7.1.2.类型
        • 7.1.3.编码和底层实现
        • 7.2.字符串对象
        • 7.3.列表对象
        • 7.4.哈希对象
        • 7.5.集合对象
        • 7.6.有序集合对象
        • 8.11.小结
    • 最后

第一部分 数据结构与对象

一、简单动态字符串

redis自己构建一种SDS的抽象类型,用作redis的默认字符串,可以用SDS的API操作

1.1 作用:
  1. redis> SET msg “hello”

    • 键是一个字符串对象,对象底层实现一个保存字符串“msg”的SDS
    • 值是一个字符串对象,对象底层实现一个保存字符串“hello”的SDS
  2. 作用于缓冲区

    • AOF模式中的缓冲区
    • 客服端的输入缓冲区
1.2. 定义:

redis的底层数据结构汇总_第1张图片

1.3. 与c语言获取str长度的区别:
  1. SDS有len属性,获取的复杂度为O(1) 。 c语言为O(N)
  2. c不知道str长度修改时容易溢出。 SDS为先检查长度是否够,不够就扩展
1.4. 减少修改字符串时带来的内存重新分配次数:

c语言每次增长或者缩短一个字符串,都要内存重分配,不做就会内泄漏。SDS通过未使用空间free解除了字符串长度和底层数组长度的关联

  1. 空间预分配

    • 如果对SDS进行修改后,SDS的长度len<1M 那么也分配给free==len
    • 如果对SDS进行修改后,SDS的长度len>=1M 那么也分配给free=1M
  2. 惰性空间释放

    • 如果缩短一个字符串,那么会把删除的位置放到free里面
1.5. 二进制安全:
  1. c语言字符串必须符合某种编码(比如ASCII)中间不能有空字符,会被认为是结尾所以不能保存图片、音频、视频等文件

  2. SDS是二进制安全的,以二进制的方式处理buf数组里的数据,写入什么读取就是什么

1.6. 兼容部分c字符串函数:

遵循C字符串以控制符结尾的惯例 这样是为了可以重用c语言的一部分库函数

1.7.小结:

redis的底层数据结构汇总_第2张图片

二、链表

2.1. 链表与链表节点的实现

链表提供了高效的节点重排能力,链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。

每个链表节点使用一个adlist.h/listNode结构来表示
redis的底层数据结构汇总_第3张图片
多个listNode通过prev和next指针组成双端链表
redis的底层数据结构汇总_第4张图片
用list来持有链表操作会更方便
redis的底层数据结构汇总_第5张图片
redis的底层数据结构汇总_第6张图片

2.2.小结

redis的底层数据结构汇总_第7张图片

三、字典

字典,乂称为符号表( symbol table )、关联数组( associative array )或映射( map ),是 一种用于保存键值对(key-value pair)的抽象数据结构

3.1.哈希表

redis的底层数据结构汇总_第8张图片

  • tabel属性是一个数组,每个元素指向dict.h/dictEntry结构的指针,dictEntry存放键值对。

  • size记录哈希表的大小,即table数组的大小

  • used记录目前已有(键值对)的数量

  • sizemask属性和哈希值一起决定一个键应该被放到table数组上的哪个索引上面

redis的底层数据结构汇总_第9张图片

3.2.哈希表节点

redis的底层数据结构汇总_第10张图片

存放键值对,next是指向下一个哈希表节点的指针(当他们的键的哈希值相等时),解决键冲突问题。

3.3.字典

redis的底层数据结构汇总_第11张图片

type属性和privdata示性式针对不同类型的键值对,为创建你多态字典设置

  • type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不用的字典设置不同的类型特定函数。

  • privddata属性则保存了需要传给那些类型特定函数的可选参数
    redis的底层数据结构汇总_第12张图片

  • ht属性是一个包含两个项的数组,数组的每一个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用

  • rehashidx,记录目前的进度,如果没有在进行rehash,那么他的值为-1

redis的底层数据结构汇总_第13张图片

3.4.哈希算法

当一个新的键值对添加到字典时,要根据键算出哈希值和索引值,然后将新键值对的哈希表节点放到哈希表数组的指定索引上面。
redis的底层数据结构汇总_第14张图片

3.5.解决键冲突
  • 相同的哈希值的键可以用next链接起来形成单向链表
  • 因为dictEntry节点组成的链表没有指向链表表尾的指针,所以考虑速度,总是将新节点添加到链表的表头位置(复杂度O(1)),排在其他已有节点的前面。k2 v2就是新插入的
    redis的底层数据结构汇总_第15张图片
3.6.rehash

哈希表保存的键值对会增多或者减少,为了让负载因子维持在一个合理的范围之内,当键值对的适量太多或者太少,就需要对哈希表的大小进行扩展或收缩,可以通过rehash(重新散列)操作来完成。
redis的底层数据结构汇总_第16张图片

步骤:

  1. 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也即是ht [ 0 ] . used属性的值):
    • 如果执行的是扩展操作,那么ht [ 1 ]的大小为第一个大于等于ht [ 0 ] . used*2 的2”( 2的n次方幂);

    • 如果执行的是收缩操作,那么ht [1]的大小为第一个大于等于ht [0] .used的2”

  2. 将保存在ht [0]中的所有键值对rehash到ht [1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[l]哈希表的指定位置上。
  3. 当ht[0]包含的所有键值对都迁移到了 ht[l]之后(ht[0]变为空表),释放 ht[0],将ht[l]设置为ht[0],并在ht[l]新创建一个空白哈希表,为下一次rehash 做准备。

redis的底层数据结构汇总_第17张图片
1)ht[0].used当前的值为4,4*2=8,所以ht[1]设置成8
redis的底层数据结构汇总_第18张图片
2)将ht[0]包含的四个键值对都rehash到ht[1]
redis的底层数据结构汇总_第19张图片
3)释放ht[0],并将ht[1]设置成ht[0]
redis的底层数据结构汇总_第20张图片

3.7.渐进式rehash

为避免rehash对服务器性能造成影响,服务器不是一性将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次,渐进式的将ht[0]里面的键值对慢慢地rehash到ht[1]
redis的底层数据结构汇总_第21张图片
一次完整的rehash过程:
redis的底层数据结构汇总_第22张图片
redis的底层数据结构汇总_第23张图片
redis的底层数据结构汇总_第24张图片
redis的底层数据结构汇总_第25张图片
redis的底层数据结构汇总_第26张图片
redis的底层数据结构汇总_第27张图片

3.8.小结

redis的底层数据结构汇总_第28张图片

四、跳跃表

跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的。
redis使用跳跃表作为有序集合键的底层实现之一,集合数量多或者元素的成员是比较长的字符串时使用。

  • Redis只在两个地方用到了跳跃表

  • 实现有序集合键

  • 在集群节点中用作内部数据结构

4.1.跳跃表的实现
  • Redis跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义
        1. zskiplistNode表示跳跃表节点
        2. zskiplist保存跳跃表节点的相关信息(节点数量,表头节点,表尾节点)
    redis的底层数据结构汇总_第29张图片

  • 最左边为zskiplist结构
        1. header: 指向跳跃表的表头节点
        2. tail:指向跳跃表的表尾节点
        3. level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不算)
        4. length:记录跳跃表的长度,跳跃表目前包含节点的数量(表头节点不计)

  • 最右边的四个zskiplistNode
        1. level:节点用L1,L2,L3等标记各个层(L1第一层) 每层两个属性:前进指针和跨度
        2. 后退指针:BW标记为后退指针
        3.分值(score):节点按各自保存的分值从小到大排列
        4.成员对象:各个节点中的o1,o2,o3是节点所保存的成员对象

4.2.跳跃表节点

redis的底层数据结构汇总_第30张图片


  •     1. 一般来说,层数越多,访问其他节点速度越快
        2.创建新节点,通过幂次定律随机生成一个1-32之前的值为level数组的大小,也就是高度

  • 前进指针以及遍历过程
    redis的底层数据结构汇总_第31张图片

  • 跨度:level[i].span属性 用于记录两个节点之间的距离(这是与普通跳跃表的区别之一)。因为在有序集合支持的命令中,有些跟元素在集合中的排名有关,比如获取元素的排名,根据排名获取、删除元素等。通过跳跃表结点的层跨度,可以快速得到该结点在跳跃表中的排名(排位rank)

  • 后退指针(与普通跳跃表的第二个区别)用于从表尾向表头访问节点。首先通过tail到最尾节点,然后向前找
    redis的底层数据结构汇总_第32张图片

  • 分值和成员:

  1. 分值(score)是一个double类型的浮点数,所有节点按照分值从小到大排序
  2. 成员对象(obj)是一个指针,指向一个字符串对象,字符串对象保存一个SDS值
  3. 对象必须唯一,分值可以相同(分值相同的节点按照成员对象在字典序中的大小来排序)
    redis的底层数据结构汇总_第33张图片
4.3.跳跃表

只靠多个跳跃表节点可以组成一个跳跃点,但是使用zskiplist结构来持有这些节点,可以更方便操作
在这里插入图片描述redis的底层数据结构汇总_第34张图片

  • header和tail指针分别指向表头和表尾,定位到表头和表尾的复杂度为O(1)。
  • length记录节点数量,可以O(1)复杂度内返回跳跃表的长度
  • level属性用于在O(1)复杂度获取最高层的节点的成数量(表头不算)
4.4.小结

redis的底层数据结构汇总_第35张图片

五、整数集合

整数集合是集合键的底层实现之一,当一个集合会包含整数值元素,并且这个集合的元素数量不多时redis使用整数集合作为集合键的底层实现。

5.1.整数集合的实现

保存的类型为int16_t、int32_t、int64_t的整数值,并且保证不会出现重复元素
redis的底层数据结构汇总_第36张图片

  • congtents数组是底层实现,每个元素都是contents数组的一个数组项,各个项在数组中按值的大小从小到大有序的排列,且没有重复项
  • 虽然声明为int8_t类型的数组,但实际取决encoding属性(16,32,64)
    redis的底层数据结构汇总_第37张图片
    encoding属性的值为INTSET_ENC_INT16 每个元素的类型为int6_t,所以contents大小为sizeof(int16_t)*5=16*5=80
5.2.升级

当我们向一个整数集合类型比较小的数组(int16_t)中加入一个比目前长的类型时(int32_t),整数集合需要先升级才能将新元素添加到整数集合。
步骤:

  1. 根据新元素类型扩展数组的大小,并未新元素分配空间
  2. 把原来数组类型换成现在的类型,且把元素放到正确的位置上,有序性不变
  3. 将新元素添加到底层数组里面
    每次添加元素都可能升级,每次升级都需要对数组中已有元素进行类型转换,所以添加的时间复杂度为O(n)
5.3.升级的的好处
  1. 提升灵活性
    c语言的限制我们不会将不同类型放在一起,但是通过这种方式就可以了
  2. 节约内存
  3. 不支持降级操作

六、压缩列表

压缩列表是列表和哈希键的底层实现之一。列表键只含有少量列表项,并且列表项要么小整数值,要么长度比较短的字符串。哈希键包含的所有键和值都是小整数值或者短字符串。

6.1.压缩列表的构成

由一系列特殊编码的连续内存块组成的顺序型数据结构。
一个压缩裂列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值
redis的底层数据结构汇总_第38张图片
redis的底层数据结构汇总_第39张图片

6.2.压缩列表节点的构成

字节数组长度选择:
<=63(2^6-1)
<=16383(z^14-1)
<=(2^32-1)
整数值长度选择:
4位长,0-12间的无符号整数
1字节或者3字节的有符号整数
int16_t 、int32_t、 int64_t
redis的底层数据结构汇总_第40张图片
previous_entry_length:
以字节为单位记录前一个字节的长度
前一个字节长度<254字节,则previous_entry_length长度为1
前一个字节长度>=254字节,则previous_entry_length长度为5
第一个字节会设置为0xFE(后4字节保留前一字节的长度)
redis的底层数据结构汇总_第41张图片
从表尾遍历到表头使用下面的原理实现,一直回溯到头
redis的底层数据结构汇总_第42张图片
encoding
记录节点的content保存的类型和长度
1字节,2字节,5字节长,值的最高位为00,01,10是字节数组编码
1字节,值的最高位为11开头的是整数编码
上面两种数组的长度都由编码除去最高两位之后的其他记录
redis的底层数据结构汇总_第43张图片
content
保存节点的值,根据上面的encoding表对照
redis的底层数据结构汇总_第44张图片

6.3.连锁更新

当所有的节点长度都为250-253字节之间,所以他们保存的都是1字节长的previous_entry_length。如果讲一个长度>=254的节点设置为表头节点,后面的e1节点保存的previous_entry_length由1变成5,后面的e2,e3…都会更新。这个过程叫连锁更新。
redis的底层数据结构汇总_第45张图片
删除节点也可能导致连锁更新—e1-en是250-253 ,big长度>=254,small<254的情况
redis的底层数据结构汇总_第46张图片

6.4.小结

redis的底层数据结构汇总_第47张图片

七、对象

redis并没有直接使用前面介绍的数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个对象系统包含字符串对象,列表对象,哈希对象,集合对象和有序集合对象

7.1.1.对象的类型与编码

键和值都是对象。每个对象都由redisObject结构表示
redis的底层数据结构汇总_第48张图片

7.1.2.类型

键总是一个字符串对象,而值可以是字符串对象,列表对象,哈希对象,集合对象和有序集合对象的一种
redis的底层数据结构汇总_第49张图片
可以用TYPE命令来获取键对应的值对象的类型

7.1.3.编码和底层实现

对象的ptr指针指向对象的底层实现数据结构,但是这个数据结构由对象的encoding属性决定。
redis的底层数据结构汇总_第50张图片
每种类型的对象至少使用了两种不同的编码
redis的底层数据结构汇总_第51张图片
OBJECT ENCODING命令可以查看一个数据库键的值对象的编码
通过encoding属性设定对象所使用的编码,提高了灵活性和效率,redis可以根据不同的场景来为一个对象设置不同的编码。

7.2.字符串对象

字符串对象的编码可以使int、raw、embstr。
如果是整数值,会将ptr属性里面(将void*变为long)编码变为int
redis的底层数据结构汇总_第52张图片
如果是字符串,并且长度大于32字节,将用SDS保存,编码为raw
redis的底层数据结构汇总_第53张图片
如果是字符串,并且长度小于等于32字节,编码为embstr
embstr是专门保存段字符串的一种优化编码方式,raw会调用两次内存分配函数来创建redisObject结构和sdshdr结构,embstr只有一次
redis的底层数据结构汇总_第54张图片
embstr的好处
redis的底层数据结构汇总_第55张图片
再有需要的时候,程序会将保存的字符串对象转为浮点数,执行操作后再转为字符串。例如数字相加的时候。

编码的转换
int和embstr在某些条件下会变为raw编码

  • int------>raw
    如果这个字符串由于某些操作导致不再是整数值,比如append操作,就会变为raw编码。
  • embstr(实际上是只读的)------>raw
    当修改embstr的时候回先变为raw,然后再执行修改命令。所以修改命令后总会变为raw。
7.3.列表对象

列表对象的编码可以是ziplist或者linkedlist

  • 每个压缩列表节点(entry)保存一个列表元素
    redis的底层数据结构汇总_第56张图片
  • linkedlist每个节点保存一个字符串对象,每个字符串对象保存一个列表元素
    在这里插入图片描述
    注意:双端两边包含了多个字符串对象,字符串对象时Redis五种类型的对象中唯一一种会被其他四种类型对象嵌套的对象
    redis的底层数据结构汇总_第57张图片
    redis的底层数据结构汇总_第58张图片

编码转换
满足下面的条件-----ziplist 任意一个不满足转换linkedlist

  • 列表对象保存的所有元素的长度都小于64字节
  • 列表对象保存的元素数量小于512个
    (上面的条件可以修改)
7.4.哈希对象

哈希对象的编码是ziplist或者hashtable

  • ziplist的实现
    redis的底层数据结构汇总_第59张图片
  • hashtable的实现(使用的是字典)
    redis的底层数据结构汇总_第60张图片
    编码转换
    满足下面的条件-----ziplist 任意一个不满足转换hashtable
  • 哈希对象保存的所有键值对的键和值的字符串的长度都小于64字节
  • 哈希对象保存的键值对数量小于512个
    (上面的条件可以修改)
7.5.集合对象

集合对象的编码是intset或者hashtable

  • intset编码的集合对象中包含的所有元素保存在整数集合里
    redis的底层数据结构汇总_第61张图片
  • hashtable底层是字典,每一个键是字符串对象,每个字符串对象包含了一个集合元素,值全部为NULL
    redis的底层数据结构汇总_第62张图片
    编码转换
    满足下面的条件-----intset 任意一个不满足转换hashtable
  • 集合对象保存的所有元素都是整数值
  • 集合对象保存的元素数量不超过512个
    (上面第二个条件可以修改)
7.6.有序集合对象

有序集合的编码是ziplist或者skiplist

  • ziplist使用压缩列表作为底层实现,第一个保存元素的成员,第二个保存分值,从小到大排序
    redis的底层数据结构汇总_第63张图片
  • skiplist使用zset结构作为底层实现(包含一个字典和一个跳跃表)
    在这里插入图片描述
    在这里插入图片描述
  1. 跳跃表可以实现对有序集合的范围操作ZRANK、ZRANGE
  2. dict字典创建一个成员到分值的映射(键保存元素的成员,值保存元素的分值) 通过字典可以O(1)复杂度查找给定成员的分值ZSCORE
  3. 有序结合每个元素的成员都是一个字符串对象,每个元素的分值都是一个double类型的浮点数
  4. 虽然zset同是使用跳跃表和字典保存有序集合元素,但是他们都会通过指针来共享相同元素的成员和分值,所以不会产生任何重复成员或者分值,也不会因此浪费额外的外存。
    redis的底层数据结构汇总_第64张图片
    redis的底层数据结构汇总_第65张图片
    注意:实际中上图的成员和分值是共享的,不会重复

编码转换
满足下面的条件-----ziplist 任意一个不满足转换skiplist

  • 有序集合保存的元素数量小于128个
  • 有序集合对象保存的所有元素成员的长度都小于64字节
    (上面的条件可以修改)
8.11.小结

redis的底层数据结构汇总_第66张图片

最后

本文是对学习redis数据结构的总结,参考书籍《redis设计与实现》

单机数据库的实现

你可能感兴趣的:(Redis理论,字符串,链表,数据结构)