Redis常见的数据结构与对象

目录

一.简单动态字符串

1.1 SDS的定义

1.2 SDS与c字符串 的区别

二.链表

2.1 链表与链表节点的实现

2.2 重点回顾

三.字典

3.1 字典实现

3.1.1 哈希表

3.1.2 哈希表节点

3.1.3 字典

3.2 哈希算法

3.3 解决键冲突

3.4 rehash

 3.5 渐进式rehash

3.6 重点回顾

四.跳跃表

4.1 跳跃表的实现

 4.1.1 跳跃表结点

4.1.2 跳跃表

4.2 重点回顾

五.整数集合

5.1 整数集合实现

六.压缩链表

6.1 压缩列表的构成

6.2 压缩列表节点的构成

6.2.1 previous_entry_length

6.2.2 encoding

 6.2.3 content

6.3 连锁反应

 6.5 重点回顾

七.各种实现对象

7.1对象的类型与编码

7.1.1 类型 

7.1.2 编码和底层实现

 7.2 字符串对象

7.2.1 编码的转换

7.2.2 字符串命令的实现

7.3 列表对象

 7.3.1 编码转换

7.3.2 列表命令的实现

 7.4 哈希对象

 7.4.1 编码转换

 7.4.2 哈希命令的实现

 7.5 集合对象

7.5.1 编码的转换

7.5.2 集合命令的实现

 7.6 有序集合对象

7.6.1 编码的转换 

7.6.2 有序集合命令的实现

最后总结

        如果有想更细节了解Redis底层实现的数据结构可以参考《Redis设计与实现》 


一.简单动态字符串

SDS除了用来保存数据库的字符串值以外,还被作为缓冲区,AOF模块中的AOF缓冲区,客户端状态中的输入缓冲区。

1.1 SDS的定义

 

1.2 SDS与c字符串 的区别

一是SDS常数复杂度获取字符串长度,因为SDS结构直接保存有字符串长度,可以直接获取,与c字符串不同,c字符串需要遍历。

二是杜绝缓冲区溢出,c字符串容易造成缓冲区溢出的情况,而SDS的空间分配策略完全杜绝了缓冲区溢出的情况,当SDS调用API要对SDS进行修改时,API会先检查SDS的所剩空间是否够用,如果不够用会进行扩容至执行修改所需的大小,然后下进行修改操作,SDS不需要手动修改空间大小,也不会出现缓冲区溢出的问题。

三是减少修改字符串时带来的重新分配内存的次数,由于内存分配的算法比较复杂,而且可能还需要执行系统调用,所以这个操作是比较耗时的,对于Redis作为数据库来说,经常被用于要求速度,而且数据会被频繁的修改的场合,为了减少内存重新分配的次数,SDS实现了空间预分配和惰性空间释放的两种优化策略。

        1.空间预分配

        空间预分配用于优化SDS的增长操作,当SDS进行空间重新分配时,不光会分配所需的空间大小,还会额外分配没使用的空间,它的分配公式如下:

  •  如果SDS在进行修改后,SDS的长度寄len的大小小于1MB,那么会分配与len相同大小的未使用空间,及len的大小与free大小相同,例如,SDS进行修改后的len会变成13,那么free大小也会变成13,实际SDS的buf数组的长度会变成13+13+1=27,其中的加一是用于保存空字符的。
  • 与上面一种情况相反,SDS进行修改后,len的大小大于等于1MB,那么SDS会分配1MB的空余空间,例如,SDS进行修改后的len大小为20MB,那么buf的实际大小为20MB+1MB+1byte。

        通过空间预分配,可以减少连续执行字符串增长操作带来的内存重新分配的次数。

        2.惰性空间释放

        惰性空间释放是用于优化字符串缩短操作的,在SDS字符串缩短后,SDS不会立马进行空闲重新分配来回收缩短后的的字节,而是用free来记录,用于将来使用。与此同时,SDS也提供相应的API,在我们需要释放空间时,真正的释放空余的空间,所以不用担心惰性空间释放造成内存浪费。

四是,二进制安全,c字符串的字符必须符合某种编码(如ASCLL),并且出字符串尾部是空字符, 字符串中不能出现空字符,否则会代码会认为第一个读入的空字符是结束位置,这些限制使c字符串只能用于保存文本数据,而不能保存图像,音频,视频等这样的二进制数据。虽然数据库一般保存文本数据,但是也有保存二进制数据的情况,Redis为确保各种适应各种场景,SDS的所有API都是二进制安全的,SDS都会以处理二进制的方式处理SDS存放在buf数组中的数据,数据写入时是什么样,数据出来就是什么样

总结

 

二.链表

        链表在Redis中使用很广泛,如列表键的底层实现之一就是链表,当一个列表键包含的数量比较多的元素,或者包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。除了列表键以外,发布于订阅,慢查询,监听器等功能也用到了链表,Redis服务器本身还是用链表来保存多个客户端的状态信息,还是用链表来构建客户端输出缓冲区。

2.1 链表与链表节点的实现

 

 

Redis的链表实现特性: 

  • 双端:链表节点都带有prev和next指针,获取某个节点的前置节点和后置节点指针的复杂度都是O(1)。
  • 无环:表头节点的prev和尾结点的next都指向null,对链表的访问都是一NULL结束。
  • 带表头和表尾指针:通过list结构head指针和tail指针,代码获取表头和表尾的复杂度是O(1)。
  • 带链表长度计数器:获取链表长度复杂度O(1)。
  • 多态:链表节点使用void*指针来保存节点值,并且通过list结构的dup,free,match三个属性为节点值设置类型特定函数,所以链表可以保存各种不同类型的值。

2.2 重点回顾

三.字典

        字典,又称符号表,关联数组,或映射,是一种用于保存键值对的一种抽象数据结构。字典中每个键都是第一无二的。

        字典在Redis中的使用非常广泛,如Redis的数据库就是使用字典来作为底层实现的,对数据库的增删改查操作也是构建在字典的操作之上的。

3.1 字典实现

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希节点,而每个哈希节点就保存了字典的一个键值对。

3.1.1 哈希表

3.1.2 哈希表节点

3.1.3 字典

 

3.2 哈希算法

        当要讲一个新键值对加入到字典中时,代码需要先根据键值对的键值计算出哈希值和对应的索引值,然后再根据索引值,将包含新的键值对的哈希表节点放到哈希表数组的指定索引上面。

 

3.3 解决键冲突

        Redis哈希表使用的是链地址法(拉链法)来解决冲突的,采用的是头插法。

3.4 rehash

        随着我们的不断操作,哈希表保存的键值对可能会逐渐增多或者减少,为了让哈希表的负载因子维持在一个合理的范围之内,当哈希表保存的键值对太多或太少,程序需要对哈希表的大小进行相应的扩展和收缩。

        扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作完成,Redis对字典的哈希表执行rehash的步骤如下:

 3.5 渐进式rehash

        rehash这个动作并不是一次性,集中式的完成的,而是分多次,渐进式的完成的。

        这样做是有原因的,当ht[0]里面的键值对数量较少时,这个rehash操作能很快完成,但是如果键值对数量达到四百万,四千万时,要一次性完成这个操作需要耗费不短的时间,这么庞大的计算量会使服务器在一段时间内停止服务,为了避免rehash对服务器性能造成的影响,服务器不是一次性的将所有键值对rehash到ht[1]中,而是分多次,渐进式的rehash。

        

3.6 重点回顾

四.跳跃表

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

        跳跃表支持平均O(logN),最坏O(N)复杂度的节点查询,还可以通过顺序性操作来批量处理节点。

        在大多数情况下,跳跃表的效率可以媲美平衡树,并且跳跃表的实现相比于平衡树要更简单,所以有不少的程序都使用跳跃表来代替平衡树。

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

4.1 跳跃表的实现

 

 4.1.1 跳跃表结点

 其中结构的属性具体意义请参考《Redis设计与实现》。

4.1.2 跳跃表

 

4.2 重点回顾

五.整数集合

5.1 整数集合实现

整数集合是Redis用于保存整数值的集合抽象数据结构,他可以保存的类型,int16,int32,int64的整数值,并且保存集合中不会出现重复的元素。

 

 

六.压缩链表

        压缩列表是列表键和哈希键的底层实现之一。当列表键或哈希键是小整数值,要么是长度比较短的字符串,Redis就会使用压缩链表作为列表键和哈希建的底层实现。

6.1 压缩列表的构成

        压缩列表是Redis为了节约内存而开发的,是有一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。

 

6.2 压缩列表节点的构成

 

6.2.1 previous_entry_length

         

        因为知道了previous_entry_length后可以通过它来计算出前一个节点的起始地址。

6.2.2 encoding

        encoding属性保存了节点content属性所保存数据的类型以及长度。

 6.2.3 content

  

6.3 连锁反应

 

 6.5 重点回顾

七.各种实现对象

7.1对象的类型与编码

Redis数据库使用对象来表示数据库中的键和值,每次当我们创建一个新的键值对时,我们至少会创建两个对象,一个作为键对象,另一个作为值对象。

7.1.1 类型 

7.1.2 编码和底层实现

 

 7.2 字符串对象

7.2.1 编码的转换

        int编码和embstr编码的字符串对象在条件满足的情况下,会被转换成raw编码的字符串对象。

对于int编码的字符串对象来说,当我们执行了某些操作后使得int编码对象不再是整数值,而是一个字符串值,那么字符串对象的编码将会从int变成row。就例如进行append操作,在一个整数值后面添加一串字符串,使得整数值变成了一个字符串。

7.2.2 字符串命令的实现

 

7.3 列表对象

  注意

 7.3.1 编码转换

         对于使用ziplist编码的列表对象来说,当上面两个条件中有任意一个不满足时,对象的编码转换操作就会被执行,原本保存在压缩列表中的元素都会被转移并保存到双向链表中去,对象的编码也会从ziplist变成linkedlist。

7.3.2 列表命令的实现

 7.4 哈希对象

        哈希对象的编码可以是ziplist也可以是hashtable

 7.4.1 编码转换

 7.4.2 哈希命令的实现

 

 7.5 集合对象

7.5.1 编码的转换

 

7.5.2 集合命令的实现

 

 7.6 有序集合对象

 

 

 

7.6.1 编码的转换 

 

7.6.2 有序集合命令的实现

最后总结

        如果有想更细节了解Redis底层实现的数据结构可以参考《Redis设计与实现》 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值