1. 简单字符串
1.1. 简介
redis中没有C语言中的传统字符串,而是自己构建了一个简单动态字符串(SDS)。在redis中,C字符串只是字符串字面量用于无须修改的字符串。可修改的就要使用到SDS了。
1.2. SDS
sds.h/sdshdr结构:
struct sdshdr{
// 记录buf数组中已使用字节的数量
// 等于SDS所保存字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
free
属性值为0,表示这个SDS没有分配任何未使用空间。len
属性的值为多少,表示这个SDS保存多少字节的字符串。buf
是char类型数组,保存着字符串,SDS遵循C字符串以空字符串结尾的惯例,结尾是‘\0’
。- SDS:里面的buf,就是我们在C中学的字符串。
1.3. SDS和C字符串的区别
C语言中字符串使用的是长度为N+1的字符数组表示一个长度为N的字符串,而且最后的字符永远都是‘\0’
。
1.3.1. 常数复杂度获取字符串长度
C字符串不会记录本身的长度信息,所以我们为了获取字符串长度,只能遍历整个字符串,进行计数,时间复杂度是O(N).
SDS中有一个len
属性记录了SDS本身的长度,时间复杂度是O(1)。
1.3.2. 杜绝缓冲区溢出
因为C字符串不记录本身的长度,所以C字符串容易造成缓冲区溢出,不要问我具体的,学过C语言的应该都知道。
SDS的空间分配策略杜绝了缓冲区溢出,当SDS API对SDS进行修改的时候,API会首先检查SDS的空间是否满足修改所需的要求,如果不满足,API会自动扩展SDS的空间至能满足的大小,再执行实际的修改操作。所以,SDS既不需要人为的修改SDS的空间大小,也不会出现缓冲区溢出的问题。
1.3.3. 减少修改字符串时带来的内存重分配次数
C字符串中,字符串的长度和底层的数组的长度之间存在着关联性,如果我们增长或者缩短一个C字符串,程序都会对这个C字符串进行一次内存分配的操作。
SDS用过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数量加一,数组中可以包含未使用的字节,这些字节由free
属性记录,而SDS就通过这些未使用的空间,实现了空间预分配和惰性空间释放两种优化策略。
1.3.3.1. 空间预分配-用于扩展
当SDS API对一个SDS进行修改并且要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所需的必须空间,还会分配一些额外的未使用空间。
额外未使用的空间数量:
- 如果SDS进行修改之后,SDS的长度(len值)小于1MB,那么分配与
len
属性一样大小的未使用空间。即free = len
. 如果SDS进行修改之后,SDS的长度(len值)大于等于1MB,那么分配1MB的未使用空间。
通过这种方法,当我们再次需要扩展的时候,就会优先考虑未使用的空间,无须执行内存重分配。
1.3.3.2. 惰性空间释放-用于缩短
当我们需要缩短SDS保存的字符串时,程序不会立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录下来,并等待将来使用。
当然SDS API提供了方法真正释放SDS的未使用空间,所以不需要担心会造成内存浪费。
1.3.4. 二进制安全
C字符串中的字符要符合编码,并且除字符串末尾,字符串内不能包含空字符(‘\0’
)。C字符把空字符作为字符串结尾的唯一标识,读到了控制符就结束了。所以C字符串,只能保存文本数据,而不能保存很多的二进制数据,例如:图像、音频之类的。
SDS API以处理二进制的方式处理SDS存放在buf
数组中的数据,程序不会对其中的数据做任何限制、过滤或者假设,数据写入的是什么,读出来就是什么。SDS通过len
属性来确定字符串结束,而不是空字符。
1.3.5. 兼容部分C字符串函数
SDS遵循了C字符串以空字符结尾的惯例,所以保存文本数据的SDS可以重用部分C字符串库中的函数<string.h>
。
1.3.6. 总结区别
C字符串 | SDS |
---|---|
获取字符串长度的复杂度是O(N) | 获取字符串长度的复杂度是O(1) |
API不安全,会造成内存溢出 | API是安全的,不会造成内存溢出 |
修改字符串长度Nci必须执行N次内存重分配 | 修改字符串长度N次最多需要执行N次内存分配 |
只能保存文本数据 | 可以保存文本和二进制数据 |
可以使用所有的<string.h> 函数 |
能使用一部分<string.h> 函数 |
1.4. SDS的主要API
函数 | 作用 | 时间复杂度 |
---|---|---|
sdsnew | 创建一个包含给定C字符串的SDS | O(N),N为给定字符串的长度 |
sdsempty | 创建一个不包含任何内容的空SDS | O(1) |
sdsfree | 释放给定的SDS | O(N),N为被释放的SDS长度 |
sdslen | 返回SDS的已使用空间字节数 | O(1),读len |
sdsavail | 返回SDS的未使用字节数 | O(1),读free |
sdsdup | 创建一个给定SDS的副本(copy) | O(N),N为给定SDS的长度 |
sdsclear | 清空SDS保存的字符串内容 | O(1),惰性空间释放策略 |
sdscat | 将给定C字符串拼接到SDS字符串末尾 | O(N),N为C字符串长度 |
sdscarsds | 将给定sds字符串拼接到另一个sds字符串末尾 | O(N),N为被拼接的SDS字符串的长度 |
sdscpy | 将给定的C字符串复制到SDS里面,覆盖SDS原有的字符串 | O(N),N为被复制的C字符串长度 |
sdsgrowzero | 用空字符将SDS扩展到指定长度 | O(N),N为扩展新增的字节数 |
sdstrim | 接收一个SDS和一个C字符串作为参数,从SDS中移除所有在C字符串中出现过的字符 | O(N^2^),N为给定C字符串的长度 |
sdsrange | 保留SDS给定区间的数据,不在区间的数据被覆盖或者清除 | O(N),N为被保留数据的长度 |
sdscmp | 对比两个SDS字符串是否相同 | O(N),N为两个SDS中较短的那个SDS的长度 |
2. 链表
链表是什么,我懒得介绍了,应该都是学过的。
链表在Redis中的应用非常广泛,什么列表键啊、保存多个客户端的状态信息、构建客户端输出缓冲区啊之类的。反正用途非常广泛。
2.1. 数据结构设计
链表节点-adlist.h/listNode结构:(双向链表)
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的数据
void *value;
}listNode;
链表-list:
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;
链表list提供了表头指针head、表尾指针tail、以及链表长度len、其余的成员可由注释直接清晰看出。
Redis 的链表实现的特性:
- 双端:采用双向链表的设计方式,含prev和next指针,获取某个节点的前置节点和后置节点的复杂度是O(1)。
- 无环:表头节点的prev和表尾节点的next都指向NULL,以NULL作为终点。
- 带表头指针和表尾指针:获取list的head和tail指针,获取头尾节点的复杂度O(1).
- 带链表长度计数器
len
,获取链表中节点数的复杂度O(1)。- 多态:链表节点使用
void*
指针来保存节点,并且可以通过dup
、free
、match
三个属性为节点值设置类型特定函数,所以链表能用来保存不同类型的值。
2.2. 链表和链表节点的API
函数 | 作用 | 时间复杂度 |
---|---|---|
listSetDupMethod | 将给定的函数设置为链表的节点值复制函数 | 复制函数可以直接通过链表的dup属性直接获得,O(1) |
listGetDupMethod | 返回链表当前正在使用节点值的复制函数 | O(1) |
listSetFreeMethod | 将给定的函数设置为链表的节点值释放函数 | 释放函数可以直接通过链表的free属性直接获得,O(1) |
listGetFree | 返回链表当前正在使用节点值的释放函数 | O(1) |
listSetMatchMethod | 将给定的函数设置为链表的节点值对比函数 | 对比函数可以直接通过链表的match属性直接获得,O(1) |
listGetMatchMethod | 返回链表当前正在使用的节点值对比函数 | O(1) |
listLength | 返回链表的长度(包含了多少个节点) | 链表长度可以通过链表的len属性直接获得,O(1) |
listFirst | 返回链表的表头节点 | head属性获得,O(1) |
listLast | 返回链表的表尾节点 | tail属性获得,O(1) |
listPrevNode | 返回给定节点的前置节点 | prev属性获得, O(1) |
listNextNode | 返回给定节点的后置节点xt | next属性获得, O(1) |
listNodeValue | 返回给定节点目前正在保存的值 | value属性获得, O(1) |
listCreate | 创建一个不包含任何节点的新链表 | O(1) |
listAddNodeHead | 将一个包含给定值的新节点添加到给定链表的表头 | O(1) |
listAddNodeTail | 将一个包含给定值的新节点添加到给定链表的表尾 | O(1) |
listInsertNode | 将一个包含给定值的新节点添加到给定节点的之前或者之后 | O(1) |
listSearchKey | 查找并返回链表中包含给定值的节点 | O(N),N为链表长度 |
listIndex | 返回链表在给定=索引上的节点 | O(N),N问链表长度 |
listDelNode | 从链表中删除节点 | O(N),N问链表长度 |
listRotate | 将链表的表尾节点弹出,然后被弹出的节点查到链表的表头,成为新的表头节点 | O(1) |
listDup | 复制一个给定链表的副本 | O(N),N问链表长度 |
listRelease | 释放给定链表,以及链表中的所有节点 | O(N),N问链表长度 |