Overview

SDS 全称是 simple dynamic string。Redis 的字符串实现是 SDS,它是一个结构体,但是却可以和 C 标准的 printf 兼容。

5 个 SDS 结构体

一般都觉得字符串就应该只有一个对应的结构体,但是 Redis 中却有 5 个。在这个 issue 之前确实只有一个,但之后就有 5 个了:https://github.com/redis/redis/pull/2509

这篇 issue 的内容概括起来就是,之前的 SDS 有 4GB 大小限制,因为现代 CPU 逐渐变成了 64 位,旧的 SDS 已经不适合用在 64 位机器上了,所以就改进了 SDS,顺便还把内存占用减小了。issue 中测试结果是减少了 16% 内存占用,带来了 25% 的性能牺牲。这个 issue 刚提出的时候,SDS 只有 4 中,后面为了进一步节省内存就多了 sds5 这种类型。以下结构体是 SDS header 的定义。

struct __attribute__ ((__packed__)) sdshdr5 {
    // 低三位是类型,高5位表示字符串长度。这里5表示的是字符串最长长度是2^5=32,
    // 但是去掉末位的 \0,所以最多只能存储长度为 31 的。
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

他们对应的字符串最大长度分别是:

sdshdr5 sdshdr8 sdshdr16 sdshdr32 sdshdr64
2^5-1 2^8-1 2^16-1 2^32-1 2^64-1

这里 buf 不会占用实际的内存空间。C 语言允许这样的写法,这样可以让实际的字符串和长度信息都在同一段内存里,而不是一个结构体存储了长度和一个字符指针。如果你申请的空间大于 sizeof(header),那么多出来的空间就可以当作 buf 字段的来使用。

实际上用户获取的指针是 buf 字段的指针,也就是 sds.buf 这个指针。

SDS 创建

SDS 创建有以下 API:

sds sdsnewlen(const void *init, size_t initlen);
sds sdstrynewlen(const void *init, size_t initlen);
sds sdsnew(const char *init);
sds sdsempty(void);
sds sdsdup(const sds s);

因为大同小异所以只介绍 sdsnewlen。

sds sdsnewlen(const void *init, size_t initlen) {
    return _sdsnewlen(init, initlen, 0);
}

_sdsnewlen是一个sds内部使用的函数,其声明是:

sds _sdsnewlen(const void *init, size_t initlen, int trymalloc);

trymalloc 标志了 malloc 失败的时候是返回错误值还是直接 abort。

它的流程如下:

  1. 根据 initlen 确定该 SDS 的类型。如果小于 32 则为 SDS5,256 则 8,以此类推。例外是 0。里面有一行,意思是长度为 0 的字符串常常要追加字符串。
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
  1. 分配对应的 header size + initlen + 1。+ 1 是为了留够一个 \0 的空间。

  2. 修改 header 的 alloc(已经分配给该 sds 的空间大小 - header size),len,修改标志 SDS 类型的 flag 字段。如果是 SDS5,那么就只存储长度到 flag 里面:

            *fp = type | (initlen << SDS_TYPE_BITS);

SDS 修改操作

SDS5 基本只会在创建的时候出现,除了 sdsIncrLen 之外,都会把它变成其他的 SDS 类型。

修改操作也是大同小异。仅介绍 cat 和 format 操作。

sdscat

/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the
 * end of the specified sds string 's'.
 *
 * After the call, the passed sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call. */
sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);

    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len);
    sdssetlen(s, curlen+len);
    s[curlen+len] = '\0';
    return s;
}

/* Append the specified null terminated C string to the sds string 's'.
 *
 * After the call, the passed sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call. */
sds sdscat(sds s, const char *t) {
    return sdscatlen(s, t, strlen(t));
}

cat 调用的 sdsMakeRoomFor 会自动转换 SDS 的类型。它调用的是 sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy)。greedy 表示给 SDS 分配的空间是刚好容下字符串,还是乘 2:

    if (greedy == 1) {
        if (newlen < SDS_MAX_PREALLOC)
            newlen *= 2;
        else
            newlen += SDS_MAX_PREALLOC;
    }

如果新的 SDS 类型不同,会销毁旧的 SDS(虽然我觉得也不一定非得销毁吧,调用 realloc 说不定也不错)。

    if (oldtype==type) {
        newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        /* Since the header size changes, need to move the string forward,
         * and can't use realloc */
        newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);
        s_free(sh);
        s = (char*)newsh+hdrlen;
        s[-1] = type;
        sdssetlen(s, len);
    }

之后就是调用 c 标准的 memcpy,重新设置长度信息。

sdscatprintf

sds sdscatprintf(sds s, const char *fmt, ...) {
    va_list ap;
    char *t;
    va_start(ap, fmt);
    t = sdscatvprintf(s,fmt,ap);
    va_end(ap);
    return t;
}

这里调用了 sdscatvprintf,它进一步调用的是 c 标准的 vsnprintf。这里有猜测字符串长度的操作。简单描述是,sdscatvprintf 有一个栈上的字符数组 staticbuf[1024],如果 fmt 长度 * 2 小于它则用这个 staticbuf,否则使用堆上分配的 buf[fmt 的长度 * 2]。里面的错误处理有两个,都是根据 vsnprintf 的返回值说明编写的(虽然我实在想不出来这输出到字符串,除了空间不够,还能够有什么错误)。

sds sdscatvprintf(sds s, const char *fmt, va_list ap) {
    va_list cpy;
...
    while(1) {
        va_copy(cpy,ap);
        bufstrlen = vsnprintf(buf, buflen, fmt, cpy);
        va_end(cpy);
        // vsnprintf 返回负数代表输出错误
        if (bufstrlen < 0) {
            if (buf != staticbuf) s_free(buf);
            return NULL;
        }
        // vsnprintf 返回值大于等于buflen代表缓冲区无法装下所有字符
        if (((size_t)bufstrlen) >= buflen) {
            if (buf != staticbuf) s_free(buf);
            buflen = ((size_t)bufstrlen) + 1;
            buf = s_malloc(buflen);
            if (buf == NULL) return NULL;
            continue;
        }
        // vsnprintf 返回非负数,并且bufstrlen小于buflen,代表成功
        break;
    }
...

关于为什么要猜测长度而不是手写一个 fmt 解析,可能是非常长的 fmt 极度少见,并且输出长度大于 1024 的非常少见吧。

顺便一提,这种用户自己包装的 printf,如果想要用上格式化检查,redis 里就有一个例子:

sds sdscatprintf(sds s, const char *fmt, ...)
    __attribute__((format(printf, 2, 3))); // 检查fmt和可变参数是否匹配。

SDS 类型转换

SDS5 基本只会在创建的时候出现,除了 sdsIncrLen、sdsResize 等以外,都会把它变成其他的 SDS 类型。

SDS 会自动升级类型(比如 SDS8 存不下 2^15 长度的字符串,就得升级为 SDS16),也会自动降级。它会调用 sdsResize。它声明如下:

sds sdsResize(sds s, size_t size, int would_regrow);

would_regrow 的含义是,如果 Resize 之后长度小于 32,那么该类型应当是 SDS8 还是 SDS5?

    if (would_regrow) {
        /* Don't use type 5, it is not good for strings that are expected to grow back. */
        if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    }