字符串数据结构总结
Redis没有使用C语言的字符串结构,而是自己设计了一个简单的动态字符串结构sds。它的特点是:可动态扩展内存、二进制安全和与传统的C语言字符串类型兼容
- sds数据结构定义
Redis 专门设计了 SDS 数据结构,在字符数组
的基础上,增加了字符数组长度和分配空间大小等元数据
typedef char *sds;
struct sds{
int free; //保存未使用的长度空间
int len; //保存已经分配的长度空间,不包括’\0’,
}
struct sdshdr{
int len;//记录buf数组中已经使用字节的数量,等于SDS所保存字符串的长度
int free;//记录buf数组中未使用字节的数量
char buf[];
};
分析:sizeof(sdshdr) = 8,这时因为sizeof(int) = 4;空数组不占用内存空间
优点:
- 常数复杂度获取字符串的长度
- 杜绝缓冲溢出
- 减少修改字符串时带来的内存分配次数(通过空间预分配和懒性空间释放实现的)
- 怎么进行预分配?
如果对SDS进行修改后,SDS的长度将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len 属性的值将和free属性的值相同
如果对SDS进行修改后,SDS的长度将大于1MB,那么程序分配1MB的未使用空间 - 怎么进行惰性空间释放
当SDS 的API 需要缩短SDS 保存的字符串时,程序并不立即使用内存重分配来回收缩短多出来的字节,而是使用free属性将这些字节的数量记录下来,并等待将来使用 - 二进制安全的:所有的SDS API 都是二进制安全的,所有SDS API都会以处理二进制的方式来处理SDS存放在buf 数组里面的数据,程序不会对其中的数据做任何限制、过滤;判断字符串是否结束通过len判断
- 兼容部分C字符串函数
- 区别如下;
- sds 基本函数
Redis在创建sds时,会为其申请一段连续的内存空间 - sds 创建函数
参数:
init :初始化字符串指针
initlen :初始化字符串的长度
返回值:
sds :创建成功返回 sdshdr 相对应的 sds 创建失败返回 NULL
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
// 根据是否有初始化内容,选择适当的内存分配方式
// T = O(N)
if (init) {
// zmalloc 不初始化所分配的内存
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
} else {
// zcalloc 将分配的内存全部初始化为 0
sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
}
// 内存分配失败,返回
if (sh == NULL) return NULL;
// 设置初始化长度
sh->len = initlen;
// 新 sds 不预留任何空间
sh->free = 0;
// 如果有指定初始化内容,将它们复制到 sdshdr 的 buf 中
// T = O(N)
if (initlen && init)
memcpy(sh->buf, init, initlen);
// 以 \0 结尾
sh->buf[initlen] = '\0';
// 返回 buf 部分,而不是整个 sdshdr
return (char*)sh->buf;
}
- sds 释放函数
sds的释放采用zfree来释放内存
void sdsfree(sds s) {
if (s == NULL) return;
// 得到内存的真正其实位置,然后释放内存
s_free((char*)s-sdsHdrSize(s[-1]));
}
- sds 动态调整函数
sds最重要的性能就是动态调整
对 sds 中 buf 的长度进行扩展,确保在函数执行之后, buf 至少会有 addlen + 1 长度的空余空间(额外的 1 字节是为 \0 准备的)
返回值
sds :扩展成功返回扩展后的 sds,扩展失败返回 NULL
复杂度 : T = O(N)
// 在原有的字符串中取得更大的空间,并返回扩展空间后的字符串
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
// 获取 s 目前的空余空间长度
size_t free = sdsavail(s);
size_t len, newlen;
// s 目前的空余空间已经足够,无须再进行扩展,直接返回
if (free >= addlen) return s;
// 获取 s 目前已占用空间的长度
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
// s 最少需要的长度
newlen = (len+addlen);
// 根据新长度,为 s 分配新空间所需的大小
if (newlen < SDS_MAX_PREALLOC)
// 如果新长度小于 SDS_MAX_PREALLOC
// 那么为它分配两倍于所需长度的空间
newlen *= 2;
else
// 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
newlen += SDS_MAX_PREALLOC;
// T = O(N)
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
// 内存不足,分配失败,返回
if (newsh == NULL) return NULL;
// 更新 sds 的空余长度
newsh->free = newlen - len;
// 返回 sds
return newsh->buf;
}
- sds 回收空余空间
回收 sds 中的空闲空间,
回收不会对 sds 中保存的字符串内容做任何修改。
返回值 : sds :内存调整后的 sds
复杂度: T = O(N)
// 用来回收sds空余空间,压缩内存,函数调用后,s会无效
// 实际上,就是重新分配一块内存,将原有数据拷贝到新内存上,并释放原有空间
// 新内存的大小比原来小了alloc-len大小
sds sdsRemoveFreeSpace(sds s) {
struct sdshdr *sh;
sh = (void*) (s-(sizeof(struct sdshdr)));
// 进行内存重分配,让 buf 的长度仅仅足够保存字符串内容
// T = O(N)
sh = zrealloc(sh, sizeof(struct sdshdr)+sh->len+1);
// 空余空间为 0
sh->free = 0;
return sh->buf;
}
- sds连接操作函数
sds提供了字符串的连接函数,用来连接两个字符串
sds sdscatlen(sds s, const void *t, size_t len) {
struct sdshdr *sh;
// 原有字符串长度
size_t curlen = sdslen(s);
// 扩展 sds 空间
// T = O(N)
s = sdsMakeRoomFor(s,len);
// 内存不足?直接返回
if (s == NULL) return NULL;
// 复制 t 中的内容到字符串后部
sh = (void*) (s-(sizeof(struct sdshdr)));
memcpy(s+curlen, t, len);
// 更新属性
sh->len = curlen+len;
sh->free = sh->free-len;
// 添加新结尾符号
s[curlen+len] = '\0';
// 返回新 sds
return s;
}
当然sds 还提供了很多其他的函数
学习的编程思想:
- 使用元数据记录字符串数组长度和封装操作的设计思想
- 节省内存(紧凑):
1‘、在redis 4.0后做了一个优化:就是设计不同的header结构,为了容纳不同长度的字符串,这也可以达到节省空间的目的
2、设置struct attribute ((packed)) sdshdr8,采用紧凑型,节约内存