Redis设计与实现——SDS实现与代码解读
@ Shen Jianan · Monday, May 30, 2016 · 3 minute read · Update at May 30, 2016

SDS原理

总体概述:是Redis中可变字符串的数据结构,所有可变的字符串都使用此数据结构。除了被用作字符串,还被用作缓冲区。

数据结构

len标记已占用长度,free标记剩余长度,buf[]是char数组。

‘\0'不算在长度中,遵循’\0'结尾的好处是可以重用C函数库的函数。如printf等。

SDS记录长度的好处

  1. SDS获得字符长度的时间是O(1)
  2. SDS可以防止字符串溢出,意外修改其它区域的数据。
  3. 使用len记录长度,不用依靠’\0'判断结尾,所以可以保存二进制数据

SDS内存分配策略

对SDS中的字符串进行修改导致字符串长度改变的时候,需要进行内存重分配。

由于内存分配耗时,很可能成为高频修改时的一个性能瓶颈。所以会进行空间预分配和惰性空间释放。

空间预分配公式

  • 如果len在修改后小于1MB,分配和len一样多的free空间。
  • 如果大于1MB,分配1MB的未使用空间。

惰性空间释放

在长度缩短的时候SDS不会主动释放free的空间,在有必要的时候才真正释放。

二进制安全

C字符串处理二进制数据可能会因为’\0'的原因提前结束。SDS以处理二进制的方式来处理buf数组的数据,不会作任何过滤。

所以SDS本质上是保存一系列二进制数据,因此SDS也能作为缓冲区的实现。保存字符只是通过保存二进制实现的功能。

代码解读

Redis中SDS的创建:

/*
 * 根据给定的初始化字符串 init 和字符串长度 initlen
 * 创建一个新的 sds
 *
 * 参数
 *  init :初始化字符串指针
 *  initlen :初始化字符串的长度
 *
 * 返回值
 *  sds :创建成功返回 sdshdr 相对应的 sds
 *        创建失败返回 NULL
 *
 * 复杂度
 *  T = O(N)
 */
/* Create a new sds string with the content specified by the 'init' pointer
 * and 'initlen'.
 * If NULL is used for 'init' the string is initialized with zero bytes.
 *
 * The string is always null-termined (all the sds strings are, always) so
 * even if you create an sds string with:
 *
 * mystring = sdsnewlen("abc",3");
 *
 * You can print the string with printf() as there is an implicit \0 at the
 * end of the string. However the string is binary safe and can contain
 * \0 characters in the middle, as the length is stored in the sds header. */
sds sdsnewlen(const void *init, size_t initlen) {

    struct sdshdr *sh;

    // 根据是否有初始化内容,选择适当的内存分配方式
    // T = O(N)
    if (init) {
        // zmalloc 不初始化所分配的内存
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    } else {
        // zcalloc 将分配的内存全部初始化为 0
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
    }

    // 内存分配失败,返回
    if (sh == NULL) return NULL;

    // 设置初始化长度
    sh->len = initlen;
    // 新 sds 不预留任何空间
    sh->free = 0;
    // 如果有指定初始化内容,将它们复制到 sdshdr 的 buf 中
    // T = O(N)
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    // 以 \0 结尾
    sh->buf[initlen] = '\0';

    // 返回 buf 部分,而不是整个 sdshdr
    return (char*)sh->buf;
}

其他诸如创建空SDS都是使用这个接口进行的封装,而其中sdsMakeRoomFor方法则体现了Redis对free空间分配的逻辑:

 /* 对 sds 中 buf 的长度进行扩展,确保在函数执行之后,
 * buf 至少会有 addlen + 1 长度的空余空间
 * (额外的 1 字节是为 \0 准备的)
 *
 * 返回值
 *  sds :扩展成功返回扩展后的 sds
 *        扩展失败返回 NULL
 *
 * 复杂度
 *  T = O(N)
 */
sds sdsMakeRoomFor(sds s, size_t addlen) {

    struct sdshdr *sh, *newsh;

    // 获取 s 目前的空余空间长度
    size_t free = sdsavail(s);

    size_t len, newlen;

    // s 目前的空余空间已经足够,无须再进行扩展,直接返回
    if (free >= addlen) return s;

    // 获取 s 目前已占用空间的长度
    len = sdslen(s);
    sh = (void*) (s-(sizeof(struct sdshdr)));

    // s 最少需要的长度
    newlen = (len+addlen);

    // 根据新长度,为 s 分配新空间所需的大小
    if (newlen < SDS_MAX_PREALLOC)
        // 如果新长度小于 SDS_MAX_PREALLOC 
        // 那么为它分配两倍于所需长度的空间
        newlen *= 2;
    else
        // 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
        newlen += SDS_MAX_PREALLOC;
    // T = O(N)
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);

    // 内存不足,分配失败,返回
    if (newsh == NULL) return NULL;

    // 更新 sds 的空余长度
    newsh->free = newlen - len;

    // 返回 sds
    return newsh->buf;
}

About Me

2018.02至今 杭州嘉云数据 算法引擎

2017.6-2017.12 菜⻦网络-⼈工智能部-算法引擎

2016.09-2018.06 南京大学研究生

2015.07-2015.09 阿里巴巴-ICBU-实习

2012.09-2016.06 南京大学本科