Redis 2.8.9源码 - Redis中的字符串实现 sds

本文为做者原创,转载请注明出处:http://my.oschina.net/fuckphp/blog/269167php


在C中子字符串的实现都是用 char *来实现的,用起来很不方便,并且容易出现内存泄露,而且效率不高,在Redis内部,字符串采用了 sds 的方式进行了封装,似的字符串在Redis中能够方便、高效的使用,Redis字符串的实现如要依赖一下两个数据类型和结构(如下代码能够在 src/sds.h中找到):redis

typedef char *sds;

sds 存放了字符串的具体值curl

struct sdshdr {
    int len;   //字符串对象已经使用的内存数量
    int free;  //字符串对象剩余的内存数量
    char buf[]; //字符串对象的具体值(其实就是sds字符串)
};

sdshdr 实现了字符串对象函数

这样设计的好处有不少,好比使得Redis在获取字符串长度的时候能够达到o(1)的复杂度,在进行追加等字符串操做的时候,能够减小内存分配(提升性能),sdshdr的结构使得根据sds字符串获取对应的sds对象的时候能够很是方便的获取。性能

  1. 建立字符串 init 为须要初始化的字符串值。initlen表示为初始化字符串的长度,该函数建立一个sds字符串对象并返回sds字符串(如下代码能够在 src/sds.c中找到):url

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh; //建立一个空的字符串对象

    //若是init为空的时候须要对分配的内存进行初始化
    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;
    sh->free = 0;
    //若是init不为空将其复制到字符串对象中
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    //在结尾加入终止符(c语言字符串以\0为结尾)
    sh->buf[initlen] = '\0';
    //返回字符串对象中的sds值
    return (char*)sh->buf;
}

例如你建立了一个sds字符串 为 hello 那么你的代码应该以下:
spa

sds str = sdsnewlen("hello", 5);

这时候,Reids会建立一个sdshdr对象,长度为:.net

sizeof(struct sdshdr) + 5 + 1

Redis在释放字符串也会分方便,由于是对整个结构进行的分配因此只须要对sds字符串的对象进行释放就能够将字符串值和字符串对象的内存都释放掉,以下:设计

void sdsfree(sds s) {
    if (s == NULL) return;
    zfree(s-sizeof(struct sdshdr));
}

释放上例中的sds字符串只须要简单的调用:指针

sdsfree(str);

  2.   根据sds字符串获取对应的字符串对象 

如1中你知道了如何建立一个字符串对象并返回它的sds字符串,根据sdshdr的存储结构,你能够方便的经过返回的sds字符串获得字符串对象,以下代码:(下面代码中s表示sds字符串,定义为 sds s;)

struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));

由1中建立的hello的字符串对象在内存中的分配大概以下图:

上例中的 s 为 sdshdr结构中的buf元素,上例代码中的 s - sizeof(struct sdshdr) 会将指向buff 的指针,移动到len上,这样经过一个简单的运算就能够获取到sds字符串的对象,并对其进行字符串操做(不知道为啥redis宁肯每次手动写,也没有对此进行一个宏定义的封装)。

  3.  计算字符串长度

计算长度的方式就很是简单了只须要根据sds字符串获取到sds对象,而后获取其len属性便可,具备o(1)的效率,而不须要去遍历字符列表,以下获取方法(如下代码能够在 src/sds.h中找到)

static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}


  4.  追加字符串

Redis的追加字符串因为其设计方式能够很是高效,进行追加,直接看代码(如下代码能够在 src/sds.c中找到)

sds sdscatlen(sds s, const void *t, size_t len) {
    struct sdshdr *sh;      //定义一个字符串对象
    size_t curlen = sdslen(s);   //获取当前sds字符串的长度 能够参考第3条

    s = sdsMakeRoomFor(s,len);   //对sds字符串扩展,申请len长度的内存(会根据free决定是否申请,见下文)
    if (s == NULL) return NULL;
    sh = (void*) (s-(sizeof(struct sdshdr)));   //根据新申请空间后的sds字符串获取对应的对象
    memcpy(s+curlen, t, len);    //将新的字符串追加到结尾
    sh->len = curlen+len;     //更新已占用空间
    sh->free = sh->free-len;  //更新剩余空间
    s[curlen+len] = '\0';     //设置字符串结尾
    return s;     //返回修改后的sds字符串
}

   对sds字符串内存进行扩展的函数以下:(如下代码能够在 src/sds.c中找到):

sds sdsMakeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;   //初始化两个字符串对象
    size_t free = sdsavail(s);   //字符串剩余内存,定义在src/sds.h中,获取方法与 sdslen()相同
    size_t len, newlen;

    if (free >= addlen) return s;
    len = sdslen(s);   //获取当前字符串长度
    sh = (void*) (s-(sizeof(struct sdshdr)));   //获取当前的字符串对象
    newlen = (len+addlen);    //计算扩展后的字符串长度
    //一下为重点:申请字符串会计算,新的长度是否会超过SDS_MAX_PREALLOC(定义在src/sds.h中,默认为1M)
    //若是超过则申请SDS_MAX_PREALLOC大小的内存,不然申请2*扩展后字符串的长度
    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的一些配置能够改变这种行为,能够经过字符串函数 sdsRemoveFreeSpace() 对多申请的那部份内存进行释放。


更多字符串函数能够参考个人另一篇文章:

Redis 2.8.9字符串操做函数头整理,并注释做用和参数说明


参考资料:

Redis2.8.9源码   src/sds.h   src/sds.c

Redis 设计与实现(初版)


很是感谢 Redis 设计与实现的做者,给我看代码带来了很大的方便。

相关文章
相关标签/搜索