Redis 3.0.4 简单动态字符串(sds)

 字符串是Redis最常见的结构,Redis没有用C中的字符串,是自己构建的simple dynamic string来表示字符串

 字符串的源码在sds.c/sds.h

  •  sds的基本结构
struct sdshdr {
    unsigned int len; //len表示当前buf中已使用字节长度
    unsigned int free;//表示buf中未使用字节的长度
    char buf[];//字节数组,保存字符串
};
  • SDS与字符串的区别

1.常数复杂度获取字符串长度

  因为sds的结构中len表示当前buf已经使用的字节长度,所以获取长度是只需要返回sds->len,时间复杂度是o(1),而C语言中必须便利整个字符串,直到遇到结尾空字符为止,所以时间复杂度是o(n).

2.杜绝缓冲区溢出

  因为C字符串不记录自身长度,所以字符串操作容易出现缓冲区溢出现象,sds的空间分配策略避免了缓冲区溢出的可能性。

3.减少修改字符串时带来的内存重新分配

    因为C字符串的长度和底层数组的长度存在关联性 ,内存重分配需要执行系统调用,执行系统调用会从用户态陷入内核态,底层依旧是通过中断实现的,所以需要保持上下文,栈信息,这个过程有时不能被中断的,所以是一个很耗时的过程。

  redis使用了空间预分配和惰性空间释放两种策略,

  空间预留,就是在字符串增长操作中,当需要扩大空间时,会对sds分配额外的空间。额外分配的空间又修改之后sds的长度决定

    1.当修改之后的长度小于SDS_MAX_PREALLOC(1M),那么分配的空间为len*2;

    2.当修改之后的长度大于SDS_MAX_PREALLOC(1M),那么分配的空间为len+1M;

    关于分配空间源码调用的事zrealloc底层调用的事realloc,如果原先的内存尾部有足够大的空间,则直接在原来的内存块尾部新增内存,如果原来的内存块尾部内存不足,或者原先的内存块无法改变大小,realloc将重新分配另一块size大小的内存,并把原来那块内存内容复制到新的内存块上。可参考:C—动态内存分配之malloc与realloc的区别

/* Enlarge the free space at the end of the sds string so that the caller
 * is sure that after calling this function can overwrite up to addlen
 * bytes after the end of the string, plus one more byte for nul term.
 *
 * Note: this does not change the *length* of the sds string as returned
 * by sdslen(), but only the free buffer space we have. */
//判断sds剩余空间是否满足addlen长度
sds sdsMakeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;
    size_t free = sdsavail(s); //返回s未使用空间长度
    size_t len, newlen;

    if (free >= addlen) return s;
    //需要扩大空间,如果修改之后的长度小于SDS_MAX_PREALLOC,则分配2*len
    //如果大于SDS_MAX_PREALLOC,则再加上SDS_MAX_PREALLOC
    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;
}

  2.惰性空间释放  

    用于优化sds字符串缩短操作,当缩短字符串时,并不会使用内存重新分配来回收缩短后多出来的字节,而是使用free属性将这些字节记录下来,待将来使用。

//出去sds字符串中左右两端字符中包含cset字符的字符
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;
    while(sp <= end && strchr(cset, *sp)) sp++;
    while(ep > start && strchr(cset, *ep)) ep--;
    len = (sp > ep) ? 0 : ((ep-sp)+1);
    if (sh->buf != sp) memmove(sh->buf, sp, len);
    sh->buf[len] = ‘\0‘;
    sh->free = sh->free+(sh->len-len);
    sh->len = len;
    return s;
}

这块是字符串缩短的代码,找了一些资料都是说是去除sds字符串中cset中包含的所有字符,但是我看源码理解的是去除sds字符中首尾中包含cset中包含的字符,比如

* Example:
 *
 * s = sdsnew("AA...AA.a.aa.Hello.World:::");
 * s = sdstrim(s,"Aa. :");
 * printf("%s\n", s);
 * OutPut:Hello.World
 * 字符串中的.是去不掉的,然后求解~

4.二进制安全

  sds用二进制的方式来处理存放在buf数组中的数据,程序不会对数据做任何限制、过滤、或者假设,数据写入是什么样的,读取时就是什么样。缓解了C语言只能存储文本文件的尴尬,sds不仅可以保存文本数据, 还可以保存任意格式的二进制数据,并且用len长度作为字符串结尾的约束。

5.兼容部分C字符串函数

  虽然sds都是二进制安全的,但是还是遵循C字符串一空字符结尾,这是为了那些保存文本文件的sds可以重用一部分<string.h>库定义的函数。 

C 字符串和 SDS 之间的区别

C 字符串SDS
获取字符串长度的复杂度为 O(N) 。获取字符串长度的复杂度为 O(1) 。
API 是不安全的,可能会造成缓冲区溢出。API 是安全的,不会造成缓冲区溢出。
修改字符串长度 N 次必然需要执行 N 次内存重分配。修改字符串长度 N 次最多需要执行 N 次内存重分配。
只能保存文本数据。可以保存文本或者二进制数据。
可以使用所有 <string.h> 库中的函数。可以使用一部分 <string.h> 库中的函数。