Redis 底层数据结构

简单动态字符串

简单动态字符串(SDS):相对于C字符串,SDS具有以下优点:常数复杂度获取字符串长度、杜绝缓冲区溢出、减少修改字符串长度时所需的内存重分配次数、二进制安全、兼容部分C字符串函数。

SDS的定义

len(字符串长度),free(未使用长度),buff(数组)。其中SDS沿用了C字符串里的空字符结尾,这样SDS直接调用C指针就可打印,不用自己构造打印函数。
C字符串获取长度需要O(N)个复杂度,SDS获取长度只需O(n)。

杜绝缓冲区溢出

由于C字符串的不记录自身长度容易造成缓冲区溢出(忘记拓展内存),而SDS则不会。

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

由于C字符串的底层存储等于字符串长度加1,所以在进行拼接或截断操作时,如果忘记拓展或释放内存将会出现缓冲区溢出或内存泄漏。
由于SDS的free属性,来关注未使用的内存。

两种优化策略

空间预分配和惰性空间。可以减少内存重分配的次数。
空间预分配:用来优化增长操作,如果修改后SDS的长度(len)小于1MB,那么free属性和len属性分配的值相同。如果修改后SDS的长度(len)大于1MB,那么free分配的值等于1MB。
惰性空间释放:用于优化缩短操作,将缩短的字节数量记录在free里面,留作将来使用。也就是释放的空间值,直接加在free里面。
在空间预分配中,当拼接的字节数小于free,那么就会直接拼接上去。但如果大于就会执行空间预分配。
二进制安全:C字符串不能包含空格,因此只能保存文本数据,不能存储如图像、视频、音频这样的二进制数据,而SDS由于其知道len长度,可以存储各种数据,写入什么样,读取就是什么样子。因此Redis可以保存任意形式的二进制数据。

链表

其实就是一个双端链表。
下图左边是一个list结构:标注了头节点和尾节点。
在这里插入图片描述
上图中的dup函数用于复制链表节点所保存的值。
free函数用于释放链表节点所保存的值
match函数用于对比链表节点所保存的值和另外一个输入值是否相同。

字典

是哈希键的底层实现之一。
哈希表的结构
在这里插入图片描述
table指针是指向dictEntry结构的数组。
哈希表节点(dictEntry)
在这里插入图片描述
key属性保存的键,而v属性表示的是值。
next树形来解决哈希冲突,连地址法(数组+链表)
下图是一个完整的哈希表+哈希节点
在这里插入图片描述
但是呢,字典结构是什么样子的呢?
在这里插入图片描述
类型特定的函数:计算哈希值函数、复制键的函数、销毁值的函数等
ht属性是一个包含两项的数组。字典只使用ht[0],而ht[1]只会在对ht[0]哈希表进行rehash时使用。

普通状态下的字典
在这里插入图片描述
rehash
当保存的键值对主键增多时,我们需要对哈希表的大小进行拓展,让哈希负载因子(已保存的节点数/哈希表大小 (used/size))维持在一个较高的合理范围之内。
拓展操作:ht[1]的大小第一个大于等于ht[0]used2的2的n次方幂。(比如ht[0]=5,used=10,那么1052=100,2的7次方为128,是第一个大于100的数。所以就会拓展128)
收缩操作:ht[1]的大小第一个小于等于ht[0]*used 的2的n次方幂(计算方法和上面拓展操作差不多)
哈希表的拓展与收缩
当没有执行BGSAVE和BGREWRITEAOF命令,并且哈希表的负载因子大于等于1或者正在执行BGSAVE和BGREWRITEAOF命令,并且并且哈希表的负载因子大于等于5。
渐进式rehash
rehash并不是一次性完成的,而是多次、渐进式完成的。注意rehashidex属性的变化。
在渐进式rehash期间,字典的删除、查找、更新等操作都会在两个哈希表上进行,并将在ht[0]查找、更新的键值对转移到ht[1]上,但是添加操作都会在ht[1]上进行,由此ht[0]的数量只减不增,最终会变成空表。

跳跃表

跳跃表是一个有序的数据结构,通过在每个节点中维持多个指向其他节点的指针,从而实现快速访问节点的目的。
平均0(logN)、最坏0(N)的复杂度的节点寻找。
跳跃表可以作为有序集合键的底层实现,当有序集合包含元素数量较多,又或者元素成员时比较长的字符串时。redis会使用跳跃表作为有序集合键的底层实现。
下图是一个跳跃表,左边是zskiplist结构包含:header(头指针)、tail(尾指针)、level(节点层)、length(长度)。
右边是节点结构(zskiplistNode):层(包含跨度和指针)、后退指针、分值、成员对象。
成员对象:由SDS(简单字符串构成),保存的成员对象是唯一的。分值是可以相同的,当分值相同的时候,可以按照其哈希值来决定谁在前,谁在后。
在这里插入图片描述
在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的
跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的哈希值大小进行排序。

整数集合

当一个集合只包含整数值元素,并且整数数量不多时候,redis就会采用整数集合作为集合键的底层实现,
整数集合的实现
在这里插入图片描述
contents数组是整数集合的底层实现:整数集合的元素都是contents数组的一个数组项,按照从大到校排列。
contents的类型取决于encoding属性的值(intset_enc-int16 intset_enc-int32 intset_enc-int64)。encoding的值不同,存储的大小也不同。

升级

当一个intset_enc-int16类型的数组添加了一个intset_enc-int64类型的数据怎么办呢?—升级
将encoding全部改为intset_enc-int64 ,所以向整数集合添加长的新元素的时间复杂度是O(n)。
升级的好处是:提升灵活性(自动转换避免类型错误)、节约内存(如果刚一开始就是intset_enc-int64 那么会浪费内存)。
不支持降级操作,即升级了以后不管这个长元素是否删除,都不会降级。

压缩列表

ziplist是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么是长度较短的字符串,要么是小整数值。那么redis就会用压缩列表来作为列表键的底层实现。
同理当一个哈希键只有少量的键值对,并且每个键值对的键和值要么是小整数、要么是短字符串,就会用压缩列表来实现哈希键。
压缩列表的构成
压缩列表为节约内存而开发出来,因为其实由连续的内存块组成的顺序型数据结构。
在这里插入图片描述
从上图,我们可以看出,压缩列表的构成。
zlbytes:整个压缩列表占用的字节
zltail:记录到尾节点至初始节点的距离
zllen:记录节点数量
zlend:表示压缩列表的末端。
压缩列表的节点构成:
压缩列表的节点可以保存一个字节数组或者一个整数值。
在这里插入图片描述
每个节点有三个属性:
previous_entry_length:前一个节点的节点长度(根据这个特性:可以实现从末尾到初始地址的遍历)。若前面一个节点的长度大于254个字节则用一个字节的空间来保存这个长度值,如果大于254,则用5个字节来保存长度值。由此可见确实压缩了内存。
encoding:content表示的是字节数组还是整数
content:存储的内容
连锁更新:由于previous_entry_length是用1个字节还是5个字节来表示前面一个节点的长度。当一个列表上面的节点全是253个字节,那么previous_entry_length全部用一个字节表示。如果此时在表头(初始位置)添加了一个255字节的节点,那么这个列表的所有节点都会进行内存重分配。这种情况不多见,基本遇不到。

参考资料

Redis设计与实现
Redis实战

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值