一、简单动态字符串SDS
- SDS: Simple Dynamic String, 简单动态字符串
1.1 SDS结构
struct sdshdr{
// 记录buf数组中已使用字节的数量
// 等于SDS所保存字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组(不是字符数组),用于保存字符串
char buf[];
}
1.2 SDS与C字符串的区别
1.2.1 获取字符串长度的时间复杂度是O(1)
- C字符串中不记录自身长度,通过判断结尾的空字符
\0
,来确定长度。所以获取字符串长度需要遍历到字符串结尾,时间复杂度是O(N); - SDS内部有
len
属性记录字符串长度,设置和更新SDS时,API会自动更新len
的值。所以,获取字符串长度可以直接读取len
的值,时间复杂度是O(1);
1.2.2 杜绝缓冲区溢出
- C字符串不记录本身长度,通过结尾的空字符
\0
判断结束,在扩展字符串时,如果不预先分配足够的空间,会造成缓冲区溢出; - SDS的API在需要对SDS进行修改时,会先检查SDS的空间是否满足需求,如果不满足会自动将SDS的空间大小扩展至所需的大小,然后才执行扩展操作;
- 总结:
- C字符串的API是不安全的,在字符串扩展时不执行空间检查与分配,需要使用者自己执行空间检查与分配操作,如果忘记执行,可能会造成缓冲区溢出;
- SDS的API是安全的,在字符串扩展时封装了空间检查与空间分配,不需要使用者自己执行,杜绝了缓冲区溢出的可能;
1.2.3 减少修改字符串时带来的内存重分配次数
- 每次增长或缩短一个C字符串时,程序都需要对保存这个C字符串的数组进行一次内存重分配操作:
- 增长字符串时,程序需要先通过内存重分配来扩展底层数组的空间大小,如果忘了这一步会产生
缓冲区溢出
; - 缩短字符串时,程序需要通过内存重分配来释放字符串不再使用的那部分空间,如果忘记这一步会产生
内存泄漏
;
- 为什么要减少内存重分配次数:
- 内存重分配涉及复杂的算法,并且可能需要执行系统调用,通常是一个比较耗时的操作;
- Redis作为数据库,经常被用于速度要求严苛,数据被频繁修改的场合,如果频繁的执行内存重分配,会对性能产生影响;
- SDS通过
free
属性记录未使用空间,实现了空间预分配和惰性空间释放两种优化策略,来减少内存重分配次数:
- 空间预分配:当SDS的API对一个SDS进行修改,并且需要进行空间扩展时,不仅会为SDS分配修改所必须的空间,还会为SDS分配额外的未使用空间(由
free
记录)。- 如果对SDS进行修改后,SDS的长度(即
len
的值)小于1MB时,那么程序分配和len属性同样大小的的未使用空间; - 如果对SDS进行修改后,SDS的长度(即
len
的值)大于等于1MB,那么程序会分配1MB
的未使用空间;
- 如果对SDS进行修改后,SDS的长度(即
- 惰性空间释放:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用
free
属性将这些字节数量记录起来,并等待将来使用。 - 总结:
- 通过空间预分配,将SDS连续增长N次字符串所需的内存重分配次数,从必定N次降低为最多N次;
- 通过惰性空间释放,SDS避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化;
1.2.4 二进制安全
- C字符串中字符必须符合某种编码(比如ASCII),由于使用空字符表示字符串结尾,所以除了字符串的末尾外,字符串里面不能包含空字符(
\0
),否则最先被程序读入的空字符将被误认为是字符串的结尾。所以C字符串只能保存文本数据,而不能保存图片、音频、视频、压缩文件这样的二进制数据; - SDS不使用空字符来判断字符串结束,而是使用
len
属性来判断,所以SDS的数组可以保存一系列的二进制数据。SDS的API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,不会对其中的数据做任何限制、过滤、假设,数据在写入时是什么样,被读取时就是什么样;
1.2.5 兼容部分C字符串函数
虽然SDS的API都是二进制安全的,但他们都遵守C字符串以空字符结尾的惯例:这些API总会将SDS保存的数据的末尾设置空字符,并且为buf数组分配空间时多分配一个字节来容纳这个空字符。这是为了让那些保存文本数据的SDS可以重用一部分C语言里的字符串函数;
二、链表
2.1 节点结构
- 链表节点使用listNode结构(链表中每个结点的结构)
typedef struct listNode{
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
}listNode;
- 每个结点都有指向前置节点的指针和指向后置节点的指针,实现的是一个
双向链表
; - 使用
void *
指针来保存节点值,可以保存各种不同类型的值;
2.2 链表结构
typedef struct list{
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 链表所包含的节点数量
unsigned long len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值比对函数
int (*match)(void *ptr, void *key);
}list;
- 获取链表长度时间复杂度O(1): 链表中带长度计数器
len
,可以在O(1)里直接读取链表长度; - 无环&#