Redis数据结构-String&LinkTable
简单动态字符串
redis底层是C语言,但是没有直接使用C语言的传统的字符串表示(以空字符结尾的字符数组),而是自己构建了一种名为简单动态字符串SDS(simple dynamic string),在redis里面,C字符串只是用在一些无须对字符串值进行修改的地方。
SDS的定义
struct sdshdr{
// 记录buf数组中已使用的字节的数量=SDS所保存的字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[]; }
buf 仍然是以C语言字符串为准,空字符串结束
len 的长度也不计算空字符串那位。
空字符串的存在对调用SDS函数的人完全透明。
Q:为什么buf仍然以C语言为准?
A:可以直接重用一部分C字符串函数库里面的函数。例如:printf("%s", s->buf);
SDS与C字符串的区别
- 常数复杂度获取字符串长度
C:遍历整个字符串
SDS:get len ,确保获取字符串长度的工作不会成为redis的瓶颈(strlen命令)
- 杜绝缓冲区溢出
C:在进行字符产strcat时,默认用户已经分配了足够的缓存,直接相加,会造成缓冲区溢出
SDS:空间分配策略(先判断空间是否满足修改要求),不满足时,先扩展,后执行修改。例如sdscat函数。
- 减少修改字符串带来的内存重分配次数
C:每次字符串修改中需要内存重分配,涉及复杂的算法,可能需要执行系统调用。
SDS:实现了空间预分配和惰性空间释放
空间预分配:用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。如果free空间满足,则无需扩展。
额外分配的未使用空间数量由以下公式决定:\
if SDS.len < 1MB : 扩展长度=修改后的SDS.len \
例如修改后为13字节,那么len=13 free=13 \
buf的实际长度= 13+13+1 = 27
if SDS.len >= 1MB : 扩展长度=1MB \
例如修改后为30MB 那么len=30MB free=1MB \
buf的实际长度=30MB+1MB+1byte
惰性空间释放:用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。意思就是字符串缩短的时候,内存并不释放,由free记录下来。SDS提供相应的API,允许你真正释放空间。
- 二进制安全
C:使用空字符串判断字符串的结束,导致无法存储存在空字符串的二进制数据类型。例如文件图像。
SDS:以len判断长度,与空字符串无关。(空字符串的结尾只是为了能使用C的部分函数,避免重复造轮子)
- 兼容部分C字符串函数
SDS可以重用一部分<string.h>库的定义函数
| 总结 | C语言 | SDS |
|---|---|---|
| 获取字符串长度的时间复杂度 | O(N) | O(1) |
| API安全 | 可能造成缓冲区溢出 | 不会造成缓存溢出(内存重分配) |
| 存储 | 只能保存文本数据 | 可以保存文本或者二进制数据 |
链表
提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活的调整链表的长度。C语言并没有内置这种数据结构,所以redis构建了自己的链表实现。
应用:列表键包含较多元素时/包含元素都是比较长的字符串时;保存多个客户端的状态信息。
链表和链表节点的实现
typedef struct listNode{
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
}listNode;
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;
dup 函数用于复制链表节点所保存的值
free 函数用于释放链表节点所保存的值
match 函数则用于对比链表节点所保存的值和另一个输入值是否相等
特性:
- 双端:可直接获取某个节点的前置节点和后置节点的复杂度
- 无环:表头节点的prev和表尾节点的next指针都指向NULL,对链表的访问是以NULL为终点
- 带表头和表尾指针
- 带链表长度计数器
- 多态:链表节点使用void *指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值