SDS的定义
redis使用自定义字符串来替代c语言传统的字符串,
struct sdshdr {
int len; // 当前字符串长度
int free; // 当前未使用的长度
char buf[]; // 字节数组,用于保存字符串,和 C 语言字符串一样用 '\0' 结尾
};
可以使用
printf("%s",s->buff)来输出
1: 降低长度获取复杂度.
通常 C 语言长度会使用 strlen() 来进行长度的计算,其原理为从开始指针往后寻找 ‘\0’ 字符来确定长度, 复杂度是 O(n) 实际上是进行了遍历,而redis使用的结构,其记录了 len 的长度,所以复杂度是 O(1) ,确保获取字符串长度的工作不会成为 redis的性能瓶颈.反复使用STRLEN命令也不会造成任何影响.
2: 杜绝缓冲区溢出
C 语言使用 strcat() 来进行字符串拼接,典型出现的问题是,当目标字符串分配的长度不够追加长度和源字符串长度时,会造成内存溢出, redis 中使用 sdscatsds() 来进行字符串拼接,在拼接时会进行长度检查,如果长度不够会进行额外的处理,生成长度适当的对象来存储
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;
}
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 sdscatsds(sds s, const sds t) {
return sdscatlen(s, t, sdslen(t));
}
3: 减少内存分配次数
上述提到, C 语言字符字进行拼接时,会进行额外的分配,如果原字符串的长度不够,需要重新进行分配操作,在一般程序中,如果修改字符串长度的情况不太常出现,那么每次修改都执行一次内存重分配是可以接受的,但是 Redis 作为数据库,经常被用于速度要求严苛,数据被频繁修改的场合,如果每进行字符串长度修改就要重分配内存,会对性能造成影响.
所以 Redis 作出以下优化:
(1): 空间预分配:
当 sds 长度小于1mb时候(len值),额外分配一倍内存,也就是如果buff长度为13字节,那么未使用空间也将是13字节,也就是buf实际长度为 13byte+13byte+1byte = 27
当 sds 长度大于1mb时,额外分配 1mb 的内存, 也就是buff识记长度为 1mb+ 1mb + 1byte
(2): 惰性空间释放
当一个字符串进行字符删减时,多出来的字符也会存放至free中,以备在未来可能的需要,总的来说,无论是预分配还是释放规则,最终目的都是用空间来减少内存的申请和释放,也是用空间换时间的思路.
sds sdstrim(sds s, const char *cset) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
char *start, *end, *sp, *ep;
size_t len;
// 设置和记录指针
sp = start = s;
ep = end = s+sdslen(s)-1;
// 修剪, T = O(N^2)
while(sp <= end && strchr(cset, *sp)) sp++;
while(ep > start && strchr(cset, *ep)) ep--;
// 计算 trim 完毕之后剩余的字符串长度
len = (sp > ep) ? 0 : ((ep-sp)+1);
// 如果有需要,前移字符串内容
// T = O(N)
if (sh->buf != sp) memmove(sh->buf, sp, len);
// 添加终结符
sh->buf[len] = '\0';
// 更新属性
sh->free = sh->free+(sh->len-len);
sh->len = len;
// 返回修剪后的 sds
return s;
}
当然如果确定某字符串不会改变时,调用下面函数来进行真正的释放,来节省空间
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;
}
4: 二进制数据存储
C 语言字符串不能存放二进制数据,如图片,视频等,原因为如果图片视频中,带有’\0’时,会被截断处理,但 Redis 作为数据库显然是要考虑图片视频等存储功能的,所以当我们使用 len 来决定长度时,就可以解决该问题,而不会出现意外而发生截断的情况.
5: 兼容 C 字符串函数
虽然 Redis 是使用长度来决定字符串的长度,但是每个字符串后还是会用’\0’来进行结尾,也就是意味着, strcat(c_string,s->buff);第二个参数可以直接使用 sds 的成员来进行调用.
总结
C 字符串 | SDS |
---|---|
获取字符串长度复杂度为O(N) | 获取字符串长度复杂度为O(1) |
API是不安全的,会造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 |
修改字符串长度N次必然需要执行N次内存分配 | 修改字符串长度N次最多需要执行N次内存分配 |
只能保存文本内容 | 可以保存文本内容,图片,以及视频 |
可以使用<string.h>中所有的函数 | 可以使用一部分<string.h>中的函数 |
Redis 只会使用 C 字符串作为字面量,大多数情况下会使用 sds 来表示字符串