1. SDS的定义
以下代码基于Redis3.0, 代码较为简单
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};
- len: 保存的字符串的长度,已经使用的字节数
- free: buf数组还可以使用的字节数
- buf: 字符数组,用来保存字符串
2. SDS与C字符串的区别
1. 获取字符串长度的时间复杂度不同
- C语言中如果想要获取一个字符串的长度,需要遍历整个字符串,直到遇到
'\0'
为止,时间复杂度为O(n) - SDS中使用一个变量len记录了字符串的长度,因此获取字符串的时间复杂度为O(1)
2. 杜绝缓冲区溢出
-
C语言中对字符串进行追加操作可能会导致缓冲区溢出,如下图所示:
-
SDS中的字符串拼接操作会先判断数组空间是否足够,不够会进行扩容,不会发生溢出现象
sds sdscatlen(sds s, const void *t, size_t len) {
struct sdshdr *sh;
size_t curlen = sdslen(s);
s = sdsMakeRoomFor(s,len);//扩容操作
if (s == NULL) return NULL;
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;
size_t free = sdsavail(s);
size_t len, newlen;
if (free >= addlen) return s;
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
if (newsh == NULL) return NULL;
newsh->free = newlen - len;
return newsh->buf;
}
在Redis中,字符串会被经常修改,如果每次修改都要进行扩容操作,会使得效率降低
3. 空间预分配
针对字符串增长操作
#define SDS_MAX_PREALLOC (1024*1024) //1MB
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
size_t free = sdsavail(s);
size_t len, newlen;
if (free >= addlen) return s;
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;//2倍扩容
else
newlen += SDS_MAX_PREALLOC;//加上1MB
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
if (newsh == NULL) return NULL;
newsh->free = newlen - len;
return newsh->buf;
}
- 增加后的字符串的长度如果小于1MB,执行2倍扩容操作
- 增加后的字符串的长度如果大于等于1MB, 容量增加1MB
- 通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数
4. 惰性空间释放
针对字符串缩短操作
一个字符串变短时,原来分配的空间变多,如果不及时释放,可能会导致内存泄漏
//从字符串s中移除字符串cset中的字符
sds sdstrim(sds s, const char *cset) {
//s指向buf s-size_struct表示sh指向sds结构体的位置
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
char *start, *end, *sp, *ep;
size_t len;
sp = start = s;//字符串s开始位置
ep = end = s+sdslen(s)-1;//字符串s结束位置
//strchr(cset, *sp): 查找sp字符是否出现在字符串cset中
while(sp <= end && strchr(cset, *sp)) sp++;
while(ep > start && strchr(cset, *ep)) ep--;
len = (sp > ep) ? 0 : ((ep-sp)+1);//len表示删除cset中字符后的字符串长度
if (sh->buf != sp) memmove(sh->buf, sp, len);//buf前面有字符出现在cset中,空间移动
//修改结构体的属性
sh->buf[len] = '\0';
sh->free = sh->free+(sh->len-len);
sh->len = len;
return s;
}
上面的代码从原始字符串s中删除字符串集合cset中的字符,并且只是从两侧进行修剪,比如s="XXXaYYYBXYYY
, cset="XY"
修剪后的结果为aYYYB
5. 二进制安全
- C语言中字符串以空字符
'\0'
判断结束,因此存储的数据中不能含有空字符 - SDS中可以使用空字符,因为SDS中是使用len属性判断字符串结束的
6. 兼容部分C字符串函数
SDS保存的数据的末尾设置为空字符,并且总会在为buf数组分配空间时多分配一个字节来容纳这个空字符,这是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数