Redis成长之路 - 数据结构

第2章 简单动态字符串

Redis使用SDS来表示字符串值,即字符串值的键值对底层都是由SDS实现的

set msg "hello"

2.1 SDS的定义

SDS以空字符结尾,保存空字符的1字节空间不计算在SDS的len属性中,以空字符结尾好处是可以之IE重用一部分C字符串函数库里的函数

2.2 SDS与C字符串的区别

C语言使用长度为N+1的字符数组来表示长度为N的字符串,并且字符数组的最后一个元素总是空字符'\0'

2.2.1 常数复杂度获取字符串长度

C字符串不记录自身的长度信息,程序必须遍历整个字符串,复杂度为O(N)。而SDS在len属性中记录了SDS本身的长度,获取SDS长度的复杂度只有O(N)

2.2.2 杜绝缓冲区溢出

SDS的空间分配策略会完全杜绝发生缓冲区溢出的可能。而C字符串,在没有提前分配足够空间的时候,会导致缓冲区溢出。当SDS API需要对SDS进行修改时,API要先检查是否满足修改的要求,如果不满足的话,API会自动将SDS的空间扩展至所需的大小,不需要手动修改SDS的大小,也就不会出现缓冲区溢出的问题。

2.2.3 减少修改字符串时带来的内存重分配次数

C字符串每次增长或者缩短长度时,都要对C字符串的数组进行一次内存的重分配,如果忘了,就会产生缓冲区溢出或者内存泄露

但是Redis 作为数据库,经常被用于速度要求严苛、数据被频繁修改的场合,如果每次修改字符串的长度都需要执行一次内存重分配的话,那么光是执行内存重分配的时间就会占去修改字符串所用时间的一大部分,如果这种修改频繁地发生的话,可能还会对性能造成影响。

通过未使用空间,SDS实现空间预分配惰性空间释放两种优化策略

1.空间预分配

空间预分配用于优化字符串增长操作:当进行字符串增长操作时,程序不仅会为SDS分配修改所需要的空间,还会为SDS分配额外的未使用的空间。

额外分配空间的数量:

  • 当所需空间大小(修改后字符串的长度)不超过1MB时,会分配与所需空间大小一样的free(未使用)空间 数组实际长度 n+n+1

  • 当所需空间大小(修改后字符串的长度)超过1MB时,会1MB大小的free空间 数组实际长度 n+1MB+1byte

当要增加的字符串长度小于free空间内存时,会之间使用free空间,不需要再进行空间预分配。

2.惰性空间释放

惰性空间释放用于优化字符串缩短:当进行字符串缩短操作时,SDS不会马上进行内存重分配来回收空间,而是用free属性将缩短的字节数量记录下来。当下次要进行字符串增长操作时,若要增长的字节数小于free空间的内存,就不会再进行内存重分配。为将来可能带来的字符串增长提供了优化。

2.2.4 二进制安全

Redis是二进制安全的,读的时候什么样,写的时候就什么样,不会破坏数据,使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据(图片、视频、音频等)

2.2.6 总结

第3章 链表

链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。

Redis构建了自己的链表实现,当一个列表键包含了数量比较多的元素,或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。

每个链表节点都是由listNode结构来实现的

双端 无环 获取链表的表头节点和表尾节点的复杂度为O(1) 多态(设置不同的类型的特定函数,可以保存不同类型的值)

第4章 字典

Redis构建了自己的字典实现,Redis的数据库就是使用字典来作为底层实现的,对数据库的增删改查操作也是构建在对字典的操作之上的。

set msg "hello world"这个键值对就保存在了代表数据库的字典里面了

字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现

4.1 字典的实现

Redis的字典使用哈希表作为原理。一个哈希表里可以有多个哈希表节点,每个哈希表节点代表一个键值对

4.1.1 哈希表

table属性是一个数组。数组中的每个元素都是一个指向dictEntry的指针

4.1.2 哈希表节点
4.1.3 字典

4.2 哈希算法

当要将一个新的键值对添加到字典时,先根据键计算出哈希值,再计算出索引值,然后再根据索引值,放到哈希表数组的指定索引上。

4.3 解决键冲突

当有两个或两个以上键被分配到同一个哈希表数组的索引上是,会发生冲突。Redis常用链地址发解决冲突,使用next指针将多个键构成单向链表来进行解决。

4.4 rehash

随着操作的不断执行,哈希表保存的键值对会主键地增多或者减少,为了让哈希表的负载因子维持在一个合理的范围内(哈希表数据分配更加合理)

收缩和扩展哈希表的操作由rehash(重新散列)来完成,对字典的哈希表执行的操作:

1.为字典的ht[1]哈希表分配空间,这个哈希表的大小取决于要执行的操作以及ht[0]中键值对的数量

  • 如果执行的是扩展操作,那么ht[1]的大小为第一个大于ht[0].used*2的2的n次方幂

  • 如果执行的是收缩操作,那么ht[1]的大小为第一个大于ht[0]*used的2的n次方幂

2.将保存在ht[0]的所有键值对rehash到ht[1]上

3.将ht[0]包含的所有建制对迁移到ht[1]上,ht[0]变为空表,释放ht[0],将ht[1]改名为ht[0],并新创建一个ht[1],为下次rehash

4.5 渐进式rehash

ht[0]里面的数据过多时,rehash并不是一次性的。若为一次性比如上亿个数据会到导致服务器停止服务

在渐进式rehash期间,ht[0]包含的键值对只减不增

第5章 跳跃表

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持指向多个其它节点的指针来达到快速访问节点的目的。

Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的数量比较多,或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。

5.1 跳跃表的实现

Redis跳跃表由redis.h/zskiplistNode(跳跃表节点)和reids.h/zskiplist(跳跃表节点的相关信息)两个结构定义

第六章 整数集合

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

6.1 整数集合的实现

contents数组是整数集合的底层实现,整数集合的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小从小到大排序,并且数组中不含重复项。

6.2 升级

当要添加一个新的元素到整数集合里面,但是新的元素的类型比现有的都要,整数集合需要先进行升级。

步骤:

1.根据新元素类型,扩展整数集合内存大小,并为新元素分配空间

2.将底层数组现有的元素都转换成与新元素相同的类型,并且全部放置到正确位置上

3.将新元素添加到底层数组中

好处:

提升灵活性,节约内存

6.4 降级

不支持

第七章 压缩列表

压缩列表是列表键和哈希键的底层实现之一。条件:列表键只含有少量列表项(小整数值,短字符串)

一个压缩列表包含多个节点,一个节点保存一个字节数组或一个整数值。

压缩列表可以实现从表尾向表头的遍历,一个节点一个节点地不断向前遍历(每个节点的previous_entry_length属性都记录了前一个节点的长度)

7.3 连锁更新

当前一节点的长度小于254字节,那么previous_entry_length需要1字节长的空间来保存这个值。当前一节点的长度大于254字节,那么previous_entry_length需要5字节长的空间来保存这个值.

此时中间位置突然增加了一个新节点,使得保存字节长度的信息不准确,需要扩展,也会影响到后续节点。

Redis将这种在特殊情况下产生的多次空间扩展操作称为连锁更新

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值