一、什么是简单动态字符串
字符串是Redis中最简单的一个数据结构,打开Redis的源码后可以发现,Redis的底层是通过C语言来实现的,然而Redis并没有使用C语言中的字符串类型,而是重构了一种新的字符串类型——简单动态字符串(simple dynamic string,SDS)。那么Redis为什么要自己去开发一种抽象字符串类型呢?
在我提供的代码中,src下的sds.h和sds.c是关于简单动态字符串的一些定义及API:
struct sdshdr {
// buf 中已占用空间的长度
intlen;
// buf 中剩余可用空间的长度
intfree;
// 数据空间
char buf[];
};
上面是SDS的结构定义,结构体中维护了一个字节数组buf[]用来保存字符串,len表示现有字符串占用的空间,free表示剩余可用的空间长度。那么这样定义有什么好处?
二、SDS的优势
我们知道,在普调字符串中如果要得到字符串的长度,因为要遍历字符串直至得到末尾的空字符“\0”,所以时间复杂度是O(N)的,而在SDS中len属性已经存储了字符串的大小,所以获取字符串的长度的时间复杂度是O(1)。我们知道,Redis作为一种高速缓存,对于读写速度的要求是十分高的,实际使用中对于字符串长度这样的参数读取是很频繁的,使用了SDS,大大提高了效率。
另外,如果用普通字符串做字符串拼接(strcat)时,如果忘记了为源字符串分配足够多的内存,那么就会发生缓冲区溢出,这样会导致最后的拼接结果与理想结果天差地别。而用SDS可以避免这样的情况。在对SDS做例如拼接这样的操作时,会先去检查SDS的剩余空间是否足够,若不够则会进行扩充。扩充策略如下:
/*
* 对 sds 中 buf 的长度进行扩展,确保在函数执行之后,
* buf 至少会有 addlen + 1 长度的空余空间
* (额外的 1 字节是为 \0 准备的)
*
* 返回值
* sds :扩展成功返回扩展后的 sds
* 扩展失败返回 NULL
*
* 复杂度
* T = O(N)
*/
sds sdsMakeRoomFor(sds s, size_t addlen) {
structsdshdr *sh, *newsh;
// 获取 s 目前的空余空间长度
size_tfree = sdsavail(s);
size_tlen, 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
returnnewsh->buf;
}
当修改SDS后,如果SDS的长度小于1MB(即SDS_MAX_PREALLOC)时,会为它分配两倍于所需空间的长度,多出来的空间会给free属性;否则,分配长度为目前长度加上 SDS_MAX_PREALLOC。这样的好处是减少因为修改字符串长度而带来的重新分配内存的次数,比如我们还需要对SDS进行拼接,因为扩充后的SDS内部已经有足够的空间,那么就不用再次重新分配空间了——分配空间是一种很耗时的操作,SDS这种预分配空间的操作大大提高了对字符串操作的效率。
另外前面提到过普通字符串的遍历是以空字符串为结尾的,但是实际的数据库也会保存例如图片这样的二进制数据,这种情况下数据中难免会存在空字符串,所以普通字符串是无法用于这样的场景的。而SDS不是用空字符串来判定字符串结尾的,所以是可以保存二进制数据的。
为了兼容C语言的字符串函数,SDS也遵循着C字符串中以空字符串结尾的格式。这样做的好处就是不用再为SDS去写针对性的字符串函数,可以直接使用C字符串库中的部分函数。
三、总结
相对于普通字符串SDS的优势主要在于以下几点:
1. 获取字符串长度的时间复杂度为O(1)
2. 自动空间预分配,不用人工分配空间,并减少了由于修改字符串长度导致了空间分配次数;也不会造成缓冲区溢出
3. 可以保存任意二进制数据,而不仅仅局限于文本数据
但SDS也有一定的局限,SDS只能用一部分<string.h>库的函数