1.简介
在c语言中,一般使用char定义字符串类型,而redis却不一样,采用sds结构进行存储。那么为什么redis弃用char而改用sds呢?这样做是基于哪些方面的考虑?这样做的优缺点各有哪些呢?
2.SDS结构
带着上面的疑问,我们先回到原点,看下sds的数据结构(以3.0版本为例)(sds结构及相关操作代码位于src/sds.h、src/sds.c文件)。
struct sdshdr {
int len;
int free;
char buf[];
};
从上面的结构可以看出,sds字符串比c字符串多2个属性,占用的字节数比c字符串多 4+4+1(sds)-1(char) = 8个。
- len: 已占用空间长度
- free:剩余空间长度
- buf: 字符串数据
下面通过简单的实例讲解sds字符串创建、追加、释放操作,以便加深对sds结构的理解。
a).sds“创建”操作:定义一个“hello”的str字符串,如下:
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
//判断是否有指定内容,如果有,则不重置分配内存的内容。如果没有,则填充0
if (init) {
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
} else {
sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
}
if (sh == NULL) return NULL;
//指定字符串长度
sh->len = initlen;
//剩余空间默认为0
sh->free = 0;
if (initlen && init)
//将字符串填充至buf
memcpy(sh->buf, init, initlen);
// 以 \0 结尾
sh->buf[initlen] = '\0';
return (char*)sh->buf;
}
str的SDS结构图:
b).sds追加:当我们将“ world”和“!”分别追加至str字符串,redis会执行以下函数进行处理:
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
//拼接字符串
sds sdscatlen(sds s, const void *t, size_t len) {
struct sdshdr *sh;
// 原有字符串长度
size_t curlen = sdslen(s);
// 扩展 sds 空间
s = sdsMakeRoomFor(s,len);
// 申请空间失败
if (s == NULL) return NULL;
//将新字符串copy至字符串尾部
sh = (void*) (s-(sizeof(struct sdshdr)));
memcpy(s+curlen, t, len);
// 更新属性
sh->len = curlen+len;
sh->free = sh->free-len;
// 添加新结尾符号
s[curlen+len] = '\0';
return s;
}
//扩展空间
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
// 获取 s 目前的剩余空间长度
size_t free = sdsavail(s);
size_t len, newlen;
//如果free>=addlen 则无需申请空间。
if (free >= addlen) return s;
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
//新字符串长度
newlen = (len+addlen);
//如果newlen<SDS_MAX_PREALLOC时,按照2倍*新字符串长度进行分配空间
//否则,申请的空间=新字符串长度+SDS_MAX_PREALLOC
//SDS_MAX_PREALLOC 默认为1024*1024 (1M)
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
//申请空间失败,返回null
if (newsh == NULL) return NULL;
//重置新字符串的剩余空间属性
newsh->free = newlen - len;
// 返回 sds
return newsh->buf;
}
追加“ world”后str的SDS结构图:
此时str->free=11,当再次追加“!”至str时,系统无需重新分配空间,只须将新字符串copy至尾部即可。SDS结构图如下:
c).sds释放空间操作,代码如下:
void sdsfree(sds s) {
if (s == NULL) return;
zfree(s-sizeof(struct sdshdr));
}
3.SDS与C字符串区别
a).sds占用空间比c字符串多8个字节4(len)+4(free);
b).相比C字符串,sds计算长度时间复杂度降低了很多,前者O(n),后者O(1).
c).减少内存分配次数:内存分配是一个费时费力的“工程”。当redis作为数据库时,数据变更会经常发生。使用SDS保存字符串数据能够有效地减少内存分配次数。
d).二进制安全:我们都知道C字符串的内容不能包含空字符,否则最先被程序读入的空字符会被误认为字符串的结束。而这一限制使得C字符串只能保存纯文本数据,无法保存像图片、视频、压缩文件等二进制文件。而sds字符串不一样,字符串长度是根据len属性来决定的,即使内容中含有空字符串,则对数据的完整性也没有任何影响。
4.兼容C字符串函数
细心的读者可以发现,sds的buf与c字符串一样,在字符串的末尾增加空字符串来表示结束。这样就可以保证sds字符串沿用C语言字符串的部分函数,而无需进行重写。
5.总结
a).sds字符串采用以“空间换时间”的做法达到提升性能的目的。
b).sds字符串功能更为强大,能支持多种格式存储数据。