目录
如果有想更细节了解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 编码的转换