《Redis设计与实现》学习笔记(未完--持续更新) 三、字典(map) 四、跳跃表(skiplist) 五、整数集合(intset)

一、字符串 SDS

Redis的底层的字符串并不是使用C语言字符串(C字符串),而是自己定义了动态字符串
五种数据类型对应的实现:String
记录长度

C字符串由于没有记录字符串长度,每次执行计算长度时都会每个字符进行计数,时间复杂度是O(N);在SDS由于记录了必要的空间长度,所以redis就算反复执行计算字符串长度时时间复杂度都是O(1)

防止缓存溢出

在C字符串中由于不记录自己的字符串长度,如果在执行修改字符串时没有提前分配空间就会造成长度溢出,而SDS记录了必要的空间长度,所以每次进行字符串修改操作时,会检查自身的空间是否足够容纳字符串的需求,检查如果空间不足时就会先进行内存重新分配才会执行操作,由于记录了必要的空间长度,所以这个检查的性能消耗是几乎可以忽略的。

减少内存重新分配

SDS通过减少了空间的重新分配,所以有效的提升了性能,SDS通过以下手段减少空间的重新分配:

  • 1、空间预分配
    在修改字符串时,SDS检查空间不足进行空间拓展时,如果拓展的空间小于1MB,就会拓展到字符串长度的两倍,例如一个字符串长度是13字节,SDS就会拓展到13+13+1(1字节是保存'\0',SDS对此不作记录),这样SDS就额外多了13字节,当再有修改操作时就会检查额外空间,记录在free字段中,当要在进行修改时SDS就会检查额外空间加现有是否足够,足够就不需要进行内存重新分配,如果拓展的空间大于1MB时,就会直接在所需空间的基础上再多分配1MB的内存空间。

通过空间的预分配策略,可以把修改N次字符串的内存重新分配从C字符串的至少N次,减少到SDS的最多N次
  • 2、惰性空间释放
    当修改字符串是一个缩短操作时,SDS并不会把空间释放掉,而是使用free记录额外空间,以备以后使用,这样就又减少了内存重新分配的次数
二进制安全

C字符串只能存储文本数据不能存储二进制数据,而Redis提供了API处理二进制数据使SDS能够存储图片、视频、压缩文件等二进制数据

兼容C字符串函数

SDS有自己的API也能使用C字符串的函数

总结:

二、双向链表(list)

Redis使用的C语言中并没有链表结构,Redis构建了自己的双向链表作为list的数据结构之一,因为双向链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表, 当list的元素比较多或者元素的长度都比较长的时候, 才从压缩列表实现转换到双向链表。

五种数据类型对应的实现:List

Redis中定义的双向链表listNode:

listNode组成双向链表,list用来持有和操作链表,结构如下:

三、字典(map)

C语言中没有内置字典的数据结构,Redis构建了自己的字典结构,Redis中的字典采用哈希表作为底层实现,一个哈希表有多个节点,每个节点保存一个键值对。Redis的数据库底层就是字典

字典是Hash的底层实现的数据结构之一,Hash默认使用压缩列表,当Hash包含的键值对比较多,或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为Hash的底层实现。

hash使用字典的条件:

  • hash对象保存的键和值字符串长度都大于64字节
  • hash对象保存的键值对数量大于512
五种数据类型对应的实现:Hash

下面是redis字典的哈希表结构定义如下:

hash表又是由dictEntry组成的,table中每个元素都会指向一个dictEntry

下面是dictEntry的结构定义,每个dictEntry就是一个键值对,结构定义如下:

字典是由哈希表组成的,哈希表由dictEntry组成,下面是字典的结构定义:

,其中type保存了一组操作键值对的函数,privdata是保存了传给函数的可选参数,ht数组中ht[0]是哈希表,h[1]是用于扩容时使用的。

下面是字典、哈希表、dictEntry的组成结构:,hash表的算法是使用sizemask计算出索引值,从而决定放在哈希数组table的哪个位置如下:其中使用链地址法解决哈希冲突

rehash的步骤:
哈希表扩展的条件

在bgsave或者bgwriteaof是数据异步备份复制的操作,异步操作会创建子进程来完成,子进程会按照写时复制来优化,所以为了避免不必要的内存写入,最大限度的节约内存,在子进程运行期间尽量减少哈希表扩展操作

当哈希表的加载因子小于0.1时会自动进入缩容操作

渐进式rehash

扩容或者缩容是将h[0]键值rehash转移到h[1],这个操作不是一次性的转移,而是渐进式的

h[0]往h[1]完成完成转移后,h[0]的空间将会被清理同时被设置为h[0],h[1]被设置为h[0],下次rehash时再进行类似的转移和身份交替
链地址法解决哈希冲突

哈希冲突时形成一个单向链表解决哈希冲突

可以看到,Redis解决哈希冲突是使用next连接相同键值形成一个链表,也就是类似java中hashmap的链地址法

总结:

四、跳跃表(skiplist)

跳跃表是有序的数据结构,平均复杂度是O(logN),最坏复杂度是O(N),跳跃表是sort set数据类型的底层数据结构实现之一

zskiplistNode是跳跃表的节点结构。zskiplist是保存跳跃表节点信息,比如节点数量,指向头尾节点的指针等信息。

五种数据类型对应的实现:SortSet

zskiplistNode的结构定义:

zskiplist的结构定义

跳跃表的结构图及属性如下:

跳表插入数据会在原有的数据上加上若干层,指向当前层的下一个节点,节点的层数是随机的生成的范围在1到32,生成原理是次幂定律(越大的数随机生成的机率越小),层数越高查询其他结点的速度就越快,而插入的顺序是按照分值进行从小到大插入,分值相等则按照存储对象的大小排序也从小到大排,每个结点存储的对象是唯一的

链表的检索效率非常低,而跳表改善解决了链表的检索效率低的问题

下面是查询23的例子,从头结点开始找,先跳到7,然后跳到19,然后到22,再到23,中间跳过了3和11

总结:

五、整数集合(intset)

整数集合(intset)是实现set数据类型的底层数据结构之一,其底层有两种实现方式,当value是整数值时,且数据量不大时使用inset来存储,其他情况都是用字典dict来存储。整数集合可以保证不会出现重复数据。
contents数组是整数集合的底层实现,整数集合的每一个元素都是contents数组的一个数组项,各个项按照数值的从小到大排序,并且不包含重复项

五种数据类型对应的实现:Set

整数集合数据结构定义如下:

encoding编码方式:共有三种,INTSET_ENC_INT16、INSET_ENC_INT32和INSET_ENC_INT64三种,分别对应不同的范围。Redis为了尽可能地节省内存,会根据插入数据的大小选择不一样的类型来进行存储。默认是INTSET_ENC_INT16

intset的结构图如下:
升级
降级

不支持降级,一旦升级就会保持升级后的状态或许再次升级的升级的状态

总结

六、压缩列表(ziplist)

压缩列表是Redis为了节约内存而开发的,是list和hash的底层实现之一。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。

五种数据类型对应的实现:List,Hash
  • List
    当一个list只包含少量列表项,并且每个列表项要么就是小整数,要么就是长度比较短的字符串,redis就会使用压缩列表来做列表键的底层实现
  • Hash
    当一个hash只包含少量键值对,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做hash的底层实现。

下面是压缩列表的组成部分:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章