花十分钟了解一下Redis五种数据类型低层数据结构

1,475 阅读8分钟

“ 本文正在参加「金石计划」 ”

1: String类型

1.1: String的三种编码类型

仔细看下面三幅图,都是设置一个key-value,但是发现三个值的编码都是不一样的,有int,embstr,raw,这也是redis中String的三种编码格式

截屏2021-12-25 下午1.44.00.png 截屏2021-12-25 下午1.45.34.png

截屏2021-12-25 下午1.46.19.png 那么这三种编码格式有什么区别呢?

格式区别
int保存long型(长整型)的64位(8个字节)有符号整数,9223372036854775807,这是最大范围,只有整数会使用int,如果是浮点数,Redis内部其实先将浮点数转为字符串,然后仔保存
embstr代表embstr格式的SDS(简单动态字符串), 保存长度小于44字节的字符串
raw保存长度大于4治疗4字节的字符串

1.2:为什么要使用三种编码格式??

是为了节约内存考虑,int需要的存储成本最低,对于一般的整数类型直接使用int存储就行,可以节省内存空间

1.3:简单动态字符串-SDS

Redis是用c实现的,c是有字符串这种数据格式的,但是Redis并没有直接复用c的,而是自己实现了一个,也就是SDS

c语言SDS
字符串长度处理需要从头开始遍历,时间复杂度为O(n)记录了当前字符串的长度,时间复杂度为O(1)
内存重新分配分配内存空间超过后,会导致数组下标越界或者内存分配溢出,也就是不会动态的去分配内存空间预分配:SDS修改后。len长度小于1M,那么将会额外分配与len相同长度的未使用空间,如果修改后长度大于1M,那么将再分配1M的使用空间。。。。。。。。。。。。。。。。。。。。。。惰性空间释放:有空间分配对应的就会有空间释放,SDS缩短时候并不会回收多余的内存空间,而是使用free字段记录下来,如果后续有变更操作,直接使用free记录的空间,减少内存分配,因为回收分配内存空间都是有消耗的
二进制安全二进制数据并不是规则的字符串格式,可能包含一些特殊的字符,比如'\O'等 ,c字符串中遇到\O就代表这个字符串结束,那\O之后的数据就读不到了根据len长度判断字符串的结束,二进制安全的问题就解决了
保存数据类型只能保存文本数据类型保存文本还有二进制数据

2:Hash类型

很多人以为Redis的Hash就是使用Hash表来实现的,其实不是啊,看下图,其实Hash的底层数据结构是分情况来实现的,一个是压缩列表-ziplist,一个是hashtable

截屏2021-12-25 下午2.12.25.png

截屏2021-12-25 下午2.15.27.png

hash-max-ziplist-entries:
使用压缩列表保存时集合中的最大元素个数,超出了就会转换成hashtable,
比如你设置成了2,然后你执行 hset people name 1 age 2 address 3,此时你设置了三个元素,分别是
name,age,address,已经大于2了,那么就不会使用ziplist了

hash-max-ziplist-value:使用压缩列表保存时集合中单个元素的最大长度,超出了就会转换成hashtable
比如你设置成了2,这时候只要有一个元素的内容长度大于2,那么也会变成hastable存储

以上二个条件只要有一个不满足就都会使用hashtable来存储

2.1:压缩列表-ziplist

  • 什么是ziplist: ziplist是一种紧凑的编码格式,总体来说是使用时间换空间的做法,就是以部分读写性能为代价来换取极高的空间利用率。因为只会用于字段个数少并且字段内容小的场景。ziplist内存利用率高与其连续内存的特性是分不来的

  • ziplist数据结构

截屏2021-12-25 下午2.30.05.png

属性长度作用
zlbytes4字节记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分配或者计算zlend的位置时使用
zltail4字节记录压缩列表尾节点距离压缩列表起始地址有多少字节,通过这个偏移量,程序无须遍历整个压缩列表就可以定位表尾巴节点的地址
zllen2字节记录了压缩列表包含的节点数量,当这个属性值大于65535时候,就需要遍历整个列表才能计算出了
entry不定存储我们的数据由三部分组成prevrawlen: 记录前一个节点所占内存的字节数,方便查找上一个元素地址。lensize, len:记录当前节点所占内存字节数,以及内容的存储类型,方便解析。content:保存了当前节点的值。
zlend1字节特殊值,用来标记压缩列表的末端
  • ziplist的缺点 - 连锁更新: 压缩列表中每一个entry都记录了上一个entry所占的字节数,如果现在在链表中间插入一个元素,那么之后的每一个元素的所占的字节数可能都会有变化,连锁更新在最坏情况下需要进行 N 次空间再分配,而每次空间再分配的最坏时间复杂度为 O(N),因此连锁更新的总体时间复杂度是 O(N^2)。

2.2:为什么要使用Hash要ziplist

在字段个数少并且字段内容比较少的情况下可以大大提高内存利用率,而且查询效率不会很慢,但是当节点数量很多的时候,Hash的时间复杂度是O(1),对于ziplist的查询效率是O(n),效率就会大大降低,主要还是在不同情况下在时间空间上的取舍

3:List类型

在现在的版本中List底层是使用了quicklist数据结构,而quicklist是ziplist和linkedlist的结合体,具体如下:

捕获.PNG

为什么要使用ziplist+linkedlist

很多人会问我直接使用其中一种不行吗?? 比如直接使用ziplist或者直接使用linkedlist,其实都行,但是会有问题

  • ziplist优点:利用连续内存的特点可以不保存prev和next指针,可以大大提高内存利用率

  • ziplist缺点:需要使用大量的连续内存,而且ziplist是牺牲了读写性能的,还有连锁更新的问题

  • linkedlist优点:不需要对数据进行压缩等操作,读写性能比ziplist好

  • linkedlist缺点:需要维护prev和next指针,需要花更多的内存

List就结合了二者的优点,在每个linkedlist节点存储ziplist的,这样可以利用ziplist提高内存利用率。同时限制了ziplist的大小,也避免了ziplist的缺点

截屏2021-12-26 下午1.55.02.png

  • list-max-ziplist-size:当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度,比如,当这个参数配置成5的时候,表示每个quicklist节点的ziplist最多包含5个数据项,当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度,这时,它只能取-1到-5这5个值
配置含义
-5每个quicklist节点上的ziplist大小不能超过64kb
-4每个quicklist节点上的ziplist大小不能超过32kb
-3每个quicklist节点上的ziplist大小不能超过16kb
-2每个quicklist节点上的ziplist大小不能超过8kb,Redis默认值
-1每个quicklist节点上的ziplist大小不能超过4kb
  • list-compress-depth: 表示一个quicklist两端不被压缩的节点个数,这里的节点指的是quicklist双向链表的节点,而不是指ziplist里面数据项的个数 | 配置 | 含义 | | --- | --- | | 0 | 是个特殊值,表示都不压缩,这是Redis默认值 | | 1 | 表示quicklist两端各有1个节点不压缩,中间节点压缩 | | 2 | 表示quicklist两端各有2个节点不压缩,中间节点压缩 | | 3 | 表示quicklist两端各有3个节点不压缩,中间节点压缩 | | n | 表示quicklist两端各有n个节点不压缩,中间节点压缩 |

4:Set类型

截屏2022-01-10 下午2.23.02.png

intset

  • 当set存储的元素都是整型并且数量小于512的时候就会使用intset,这里需要满足二个条件,整型小于512

截屏2022-01-10 下午2.31.33.png

hashtable

  • 存储的不是整型或者元素个数大于512就使用hashtable来存储

截屏2022-01-10 下午2.31.59.png

为什么要使用intset

intset底层使用连续内存来存储元素,所以对内存有一定的要求,因为使用的是数组来存储的,在存储元素的时候会进行排序,主要是在查找元素的时候是使用二分查找法来查找,所以需要保证数组的有序。hashtable底层因为要涉及到扩容,rehash,存储元素的时候还需要多保存一个null的value值。intset会更节省内存