Redis 源码分析 一 基本数据结构

Redis 源码分析总览

https://blog.csdn.net/men_wen/article/details/75668345

笔记汇总:

使用双向链表的好处:

  • prev和next指针:获取某个节点的前驱节点和后继节点复杂度为O(1)。

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

(二)--- 简单动态字符串

1.介绍
Redis兼容传统的C语言字符串类型,但没有直接使用C语言的传统的字符串(以’\0’结尾的字符数组)表示,而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的对象。简单动态字符串在Redis数据库中应用很广泛,例如:键值对在底层就是由SDS实现的

3. SDS的优点
SDS本质上就是char *,因为有了表头sdshdr结构的存在,所以SDS比传统C字符串在某些方面更加优秀,并且能够兼容传统C字符串。

3.1 兼容C的部分函数
因为SDS兼容传统的C字符串,采用以’\0’作为结尾,所以SDS就能够使用一部分

3.2 二进制安全(Binary Safe)
因为传统C字符串符合ASCII编码,这种编码的操作的特点就是:遇零则止 。即,当读一个字符串时,只要遇到’\0’结尾,就认为到达末尾,就忽略’\0’结尾以后的所有字符。因此,如果传统字符串保存图片,视频等二进制文件,操作文件时就被截断了。

而SDS表头的buf被定义为字节数组,因为判断是否到达字符串结尾的依据则是表头的len成员,这意味着它可以存放任何二进制的数据和文本数据,包括’\0’

3.3 获得字符串长度的操作复杂度为O(1)
传统的C字符串获得长度时的做法:遍历字符串的长度,遇零则止,复杂度为O(n)。

而SDS表头的len成员就保存着字符串长度,所以获得字符串长度的操作复杂度为O(1)。

3.4 杜绝缓冲区溢出
因为SDS表头的free成员记录着buf字符数组中未使用空间的字节数,所以,在进行APPEND命令向字符串后追加字符串时,如果不够用会先进行内存扩展,在进行追加。

总之,正是因为表头的存在,使得redis的字符串有这么多优点。

4. SDS源码剖析
4.1 SDS内存分配策略—空间预分配
空间预分配策略用于优化SDS的字符串增长操作。

如果对SDS进行修改后,SDS表头的len成员小于1MB,那么就会分配和len长度相同的未使用空间。free和len成员大小相等。
如果对SDS进行修改后,SDS的长度大于等于1MB,那么就会分配1MB的未使用空间。
通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。

4.2 SDS内存释放策略—惰性空间释放

惰性空间释放用于优化SDS的字符串缩短操作。

  • 当要缩短SDS保存的字符串时,程序并不立即使用内存充分配来回收缩短后多出来的字节,而是使用表头的free成员将这些字节记录起来,并等待将来使用。

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

(三) Redis 字典结构

Redis 字典结构
1. 介绍
字典又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对(key-value pair)的抽象数据结构。例如:redis中的所有key到value的映射,就是通过字典结构维护,还有hash类型的键值。
redis的字典是由哈希表实现的,一个哈希表有多个节点,每个节点保存一个键值对

 

3.2 MurmurHash2哈希算法

当字典被用作数据库的底层实现,或者哈希键的底层实现时,redis用MurmurHash2算法来计算哈希值,能产生32-bit或64-bit哈希值

4. rehash
当哈希表的大小不能满足需求,就可能会有两个或者以上数量的键被分配到了哈希表数组上的同一个索引上,于是就发生冲突(collision),在Redis中解决冲突的办法是链接法(separate chaining)。但是需要尽可能避免冲突,希望哈希表的负载因子(load factor),维持在一个合理的范围之内,就需要对哈希表进行扩展或收缩。

Redis对哈希表的rehash操作步骤如下:

扩展或收缩
扩展:ht[1]的大小为第一个大于等于ht[0].used * 2的 2n2n 。
收缩:ht[1]的大小为第一个大于等于ht[0].used的 2n2n 。
将所有的ht[0]上的节点rehash到ht[1]上。
释放ht[0],将ht[1]设置为第0号表,并创建新的ht[1]。

5. 渐进式rehash(incremental rehashing)
渐进式rehash的关键:

字典结构dict中的一个成员rehashidx,当rehashidx为-1时表示不进行rehash,当rehashidx值为0时,表示开始进行rehash。
在rehash期间,每次对字典的添加、删除、查找、或更新操作时,都会判断是否正在进行rehash操作,如果是,则顺带进行单步rehash,并将rehashidx+1。
当rehash时进行完成时,将rehashidx置为-1,表示完成rehash。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

(四) Redis  跳跃表(skiplist)

1. 跳跃表(skiplist)介绍
定义:跳跃表是一个有序链表,其中每个节点包含不定数量的链接,节点中的第i个链接构成的单向链表跳过含有少于i个链接的节点。
跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,大部分情况下,跳跃表的效率可以和平衡树相媲美。
跳跃表在redis中当数据较多时作为有序集合键的实现方式之一。

3. 幂次定律
在redis中,返回一个随机层数值,随机算法所使用的幂次定律。

含义是:如果某件事的发生频率和它的某个属性成幂关系,那么这个频率就可以称之为符合幂次定律。
表现是:少数几个事件的发生频率占了整个发生频率的大部分, 而其余的大多数事件只占整个发生频率的一个小部分。

4. 跳跃表与哈希表和平衡树的比较
跳跃表和平衡树的元素都是有序排列,而哈希表不是有序的。因此在哈希表上的查找只能是单个key的查找,不适合做范围查找。
跳跃表和平衡树做范围查找时,跳跃表算法简单,实现方便,而平衡树逻辑复杂。
查找单个key,跳跃表和平衡树的平均时间复杂度都为O(logN),而哈希表的时间复杂度为O(N)。
跳跃表平均每个节点包含1.33个指针,而平衡树每个节点包含2个指针,更加节约内存。
因此,在redis中实现有序集合的办法是:跳跃表+哈希表

跳跃表元素有序,而且可以范围查找,且比平衡树简单。
哈希表查找单个key时间复杂度性能高。
4. 跳跃表基本操作
redis关于跳跃表的API都定义在t_zset.c文件中。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

(五) Redis  整数集合(intset)

1. 介绍
整数集合(intset)是集合键底层实现之一。集合键另一实现是值为空的散列表(hash table),虽然使用散列表对集合的加入删除元素,判断元素是否存在等等操作时间复杂度为O(1),但是当存储的元素是整型且元素数目较少时,如果使用散列表存储,就会比较浪费内存,因此整数集合(intset)类型因为节约内存就存在

3. 升级
intset整数集合之所以有三种表示编码格式的宏定义,是因为根据存储的元素数值大小,能够选取一个最”合适”的类型存储,”合适”可以理解为:既能够表示元素的大小,又可以节省空间。

因此,当新添加的元素,例如:65535,超过当前集合编码格式所能表示的范围,就要进行升级操作。
 

3.2 调整内存空间

当得到新元素的编码格式后,就要将集合中所有元素的编码格式都要变成升级后的编码格式,因此,需要调整集合数组contents的内存空间大小,调用intsetResize()函数

3.3 根据编码格式设置对应的值

调整好内存空间后就根据编码格式来设置集合元素的值和最后将新元素添加到集合中,都调用_intsetSet()函数。

3.5 升级的特点
提升灵活性:因为C语言是静态类型的语言,通常在在数组中只是用一种类型保存数据,例如,要么只用int16_t类型,要么只用int32_t类型。通过自动升级底层数组来适应不同类型的新元素,不必担心类型的错误。
节约内存:整数集合既可以让集合保存三种不同类型的值,又可以确保升级操作只在有需要的时候进行,这样就节省了内存。
不支持降级:一旦对数组进行升级,编码就会一直保存升级后的状态。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

(六) Redis  压缩列表(ziplist)

1. 介绍

压缩列表(ziplist)是哈希键的底层实现之一。它是经过特殊编码的双向链表,和整数集合(intset)一样,是为了提高内存的存储效率而设计的。当保存的对象是小整数值,或者是长度较短的字符串,那么redis就会使用压缩列表来作为哈希键的实现。

redis没有提供一个结构体来保存压缩列表的信息,而是提供了一组宏来定位每个成员的地址,定义在ziplist.c文件中:

由于压缩列表对数据的信息访问都是以字节为单位的,所以参数zl的类型是char *类型的,因此对zl指针进行一系列的强制类型转换,以便对不用长度成员的访问

 

5. 连锁更新
连锁更新的两种情况:

如果前驱节点的长度小于254,那么prev_entry_len成员需要用1字节长度来保存这个长度值。
如果前驱节点的长度大于等于254,那么prev_entry_len成员需要用5字节长度来保存这个长度值。
如果一个压缩列表中,有多个连续、长度介于250字节到253字节之间的节点,因此记录这些节点只需要1个字节的prev_entry_len,如果要插入一个长度大于等于254的新节点到压缩列表的头部,然而原来的节点的prev_entry_len成员长度仅仅为1个字节,无法保存新节点的长度,因此会对新节点之后的所有prev_entry_len成员大小为1字节的节点产生连锁更新。同样的,如果一个压缩列表中,是多个连续的长度大于等于254的节点,当往压缩列表的头部插入一个长度小于254的节点,也会产生连锁更新。另外删除节点也会产生连锁更新。

在redis中,只处理第一种情况,不处理因为节点的变小而引发的连锁更新,防止出现反复的缩小-扩展(flapping,抖动)
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

(七) Redis  快速列表(quicklist)

1. 介绍

quicklist结构是在redis 3.2版本中新加的数据结构,用在列表的底层实现。

quicklist结构在quicklist.c中的解释为A doubly linked list of ziplists意思为一个由ziplist组成的双向链表

首先回忆下压缩列表的特点:

    压缩列表ziplist结构本身就是一个连续的内存块,由表头、若干个entry节点和压缩列表尾部标识符zlend组成,通过一系列编码规则,提高内存的利用率,使用于存储整数和短字符串。
   压缩列表ziplist结构的缺点是:每次插入或删除一个元素时,都需要进行频繁的调用realloc()函数进行内存的扩展或减小,然后进行数据”搬移”,甚至可能引发连锁更新,造成严重效率的损失

接下来介绍quicklist与ziplist的关系:

之前提到,quicklist是由ziplist组成的双向链表,链表中的每一个节点都以压缩列表ziplist的结构保存着数据,而ziplist有多个entry节点,保存着数据。相当与一个quicklist节点保存的是一片数据,而不再是一个数据。

根据以上描述,总结出一下quicklist的特点:

quicklist宏观上是一个双向链表,因此,它具有一个双向链表的有点,进行插入或删除操作时非常方便,虽然复杂度为O(n),但是不需要内存的复制,提高了效率,而且访问两端元素复杂度为O(1)。
quicklist微观上是一片片entry节点,每一片entry节点内存连续且顺序存储,可以通过二分查找以 log2(n) 的复杂度进行定位。
总体来说,quicklist给人的感觉和B树每个节点的存储方式相似

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值