前言
好早以前就像研究一下Redis了,一直以前都没有时间和机会,其实都是自己找的借口而已。做技术的基础要打牢,下面准备跟着黄建宏老师的Redis的 设计与实现 一书,学习巩固一下最基本的数据结构。
Redis作为一个纯C语言写成的高性能中间件,不像Java、Python等高级语言内置很多数据结构,研究其数据结构的写法肯定会有着不小的收获。
接下来我会根据我的进度整理学习笔记,做一个跟着Redis学数据结构的专题。
先从字符串开始,Redis 内使用 SDS 处理字符串
1、SDS说明
- SDS 即simple dynamic string,简单动态字符串;
- SDS基于C语言的字符数组进行一层封装,并提供操作API;
SDS 相对于char字符数组的优势在于:
- 获取字符串的长度的事件复杂度为O(1);
- API 安全,即通过API操作sds不会造成缓冲区溢出;
- 每次修改字符串不一定需求进行内存分配,提高性能;
- 可以保存文本和二进制数据(使用长度表示结束),char数组不能存二进制(因为 表示结束);
2、SDS
sds的声明和实现位于redis源码目录下 src/sds.h中,sds.c是API的实现
这两个文件并没有引入因他库的头文件,sdshdr 就是用最基本的c语言写的数据结构
sds的声明如下:
typedef char *sds;//这里的sds就是sdshdr的buf指针
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};
sdshdr 内包含一个存储字符串的字符数组,一个表示存储的字符串长度的变量,以及一个表示分配了内存但是没有使用到的的字符串长度
sds 指针指向创建的 sdshdr 内的字符数组(从创建API中可以看出)
对比于C语言的字符数组,sds 有几种特性:
- 保存了字符串长度;
- 避免缓冲区溢出;
- 惰性空间释放,减少重新分配内存;
- 二进制安全;
- 兼容C语言字符串函数;
2.1、保存了字符串长度
sdshdr 内的 len 变量保存了 buff 中存储的字符串的长度,不包括
但是 len 的值,不完全等于 strlen(buff),因为 buff 中可能存储的是二进制数据,即当存储文本数据时,len 等于 strlen(buff),当存储二进制数据的时候,len 不等于 strlen(buff)
C语言的 strlen() 方法获取字符串长度需要遍历一遍字符串,知道 位置,时间复杂度为 O(N),sdshdr 获取字符串长度直接使用 len 即可,时间复杂度为 O(1),这是一种典型的以空间换时间的做法
2.2、避免缓冲区溢出
sds 的API在就行字符串的添加的时候,都会先检查现有内存是否足够,若不足够则先分配内存。这样就避免了因忘记分配内存而添加数据导致的缓冲区溢出的问题出现
2.3、惰性空间释放
sds API 在添加数据的时候,会分配内存,但是缩减的时候并不会立马释放内存,因为频繁地操作内存,会影响效率,sds 有专门的API用于释放内存
sds 的空间预分配决定如下:
- 若 len 长度小于1M,则分配内存之后,len 会等于 free,即会申请所需空间的两倍,一份留作使用,一份预留使用;
- 若 len 长度大于1M,则会额外多申请1M的空间预留,即free 为1M
2.4、二进制安全
C语言的字符数组只能存储文本,因为解析的时候以 表示结束,但是若是二进制数据(其中可能包括 ),就会出现数据遗漏
而 sds 是以 len 长度作为判断结束的依据,因此,其可以保存二进制数据,是二进制安全的
2.5、兼容C语言字符串函数
从源码中可以看出,sds 指针指向 sdshdr 的 buf,如下图所示:
sds 类型其实也就是 char*,因此它能兼容一部分C语言的库函数甚至 string 类的函数
3、部分源码解析
redis 的sds API 的参数/返回值都是 sds 类型,没有 sdshdr 这种结构体类型的,因为要尽量兼容C语言的库函数,所以,一般使用 sds 类型操作
typedef char *sds;//这里的sds就是sdshdr的buf指针
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};
下面方法用于获取sds字符串长度
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
这里这里的 sizeof(struct sdshdr),这里涉及到以前的只是盲区了,这个sizeof() 的返回值是8,具体为什么是8,后面讲解。
s 指针指向 buf,s-8 指向该 s 所处的的 sdshdr 类型,即为 sdshdr 指针,然后返回其成员 len 即可
这里有个问题,为什么sizeof(struct sdshdr)结果会是8?
3.1、柔性数组
这里涉及到两个C语言的知识点:
- 结构体中的元素内存对其;
- 结构体中的柔型数组;
内存对其不消多说,主要谈谈柔性数组
柔性数组(flexible array member)又称伸缩性数组成员,这种数组主要是为了结构体而产生的。因为开发时,偶尔需要在结构体中存放长度可变的数组,一般情况下,咱们会定义一个数组指针,需要时,分配内存使用,这样有个缺点就是,内存利用的效率很低,所以柔性数组作用就像动态数组一样,可以在结构体中存放一个长度动态的字符串
-
柔性数组在结构体中的使用从C99开始;
-
柔性数组对于编译器来说,长度为0,不占结构体内存;
-
柔性数组必须放在结构体的最后;
-
柔型数组不是数组指针,它是一个偏移量;
所以,这样就好理解了,sdshdr 的 sizeof() 值只需要计算 len 和 free 即可,所以结果为8