前言
上一篇介绍了 Redis 是如何存储和索引数据的,这一篇将介绍 Redis 定义的八大数据结构,这些高效的数据结构,不仅帮助 Redis 实现了高性能,更能够应用于各种场景。
八大数据结构如下:SDS、双向链表、压缩列表、哈希表、整数集合、跳表、quicklist、listpack。
SDS
simple dynamic string,简单动态字符串。
Redis 是用 C 语言实现的,但是它没有直接使用 C 语言的 char* 字符数组来实现字符串,而是自己封装了一个名为 SDS 的数据结构来表示字符串,主要是因为 C 语言提供的字符串实现方式有部分缺陷。
C语言字符串缺陷
C 语言的字符串其实就是一个字符数组,即数组中每个元素是字符串中的一个字符。最后一个字符是“\0”,表示字符串的结束。
缺陷:
- C 语言获取字符串长度的时间复杂度是 O(N),需要遍历数组寻找“\0”的位置;
- 字符串的结尾是以 “\0” 字符标识,所以字符串里面不能含有 “\0” 字符,否则最先被程序读入的 “\0” 字符将被误认为是字符串结尾,这个限制使得 C 语言的字符串只能保存文本数据,不能保存像图片、音频、视频文化这样的二进制数据;
- C 语言标准库中字符串的操作函数既不高效又不安全,容易导致缓冲区溢出;
结构设计
SDS 结构中的每个成员变量如下:
- len,记录了字符串长度,占 4 个字节。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1);
- alloc,分配给字符数组的空间长度,占 4 个字节。用于自动扩容;
- flags,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64;
- buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。为了兼容部分 C 语言标准库的函数, 结尾还是会加上 “\0” 字符,额外占 1 个字符。
flags 表示的是 SDS 类型,不同类型的区别在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同。之所以设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。
除了设计不同类型的结构体,Redis 在编程上还使用了专门的编译优化来节省内存空间,即在 struct 声明了
__attribute__ ((packed))
,它的作用是:告诉编译器取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐。默认情况下,编译器是按照字节对齐的方式给变量分配内存的,比如一个 char 类型变量和一个 int 类型变量,会被分配到 8 个字节(int 占四个字节,char 占一个字节但是会被分配到四个字节)。
SDS 相比于 C 的原生字符串:
- SDS 不仅可以保存文本数据,还可以保存二进制数据。因为
SDS
使用len
属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在buf[]
数组里的数据。所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。 - SDS 获取字符串长度的时间复杂度是 O(1)。因为 C 语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而 SDS 结构里用
len
属性记录了字符串长度,所以复杂度为O(1)
。 - Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。
自动扩容
在修改字符串的时候,可以通过 alloc - len
计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小。就不会出现前面所说的缓冲区溢出的问题。
当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小:
- 如果所需的 SDS 长度小于 1 MB,那么最后的扩容是按照翻倍扩容来执行的,即 2 倍的 newlen
- 如果所需的 SDS 长度超过 1 MB,那么最后的扩容长度应该是 newlen + 1MB。
在扩容 SDS 空间之前,SDS API 会优先检查未使用空间是否足够,如果不够的话,API 不仅会为 SDS 分配修改所必须要的空间,还会给 SDS 分配额外的「未使用空间」,从而减少内存分配次数。
双向链表
C 语言本身没有链表这个数据结构的,所以 Redis 自己设计了一个链表数据结构。
typedef struct list {
//链表头节点
listNode *head;
//链表尾节点
listNode *tail;
//节点值复制函数
void *(