redis的简单动态字符串(SDS, Simple Dynamic String)

28 阅读7分钟

简单动态字符串(SDS, Simple Dynamic String)

redis里面将SDS用作redis的默认字符串表示。包含字符串的键值对在底层的实现都是由SDS实现的。

//sds.h
/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    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[];
};

定义了5种用于实现 Redis 的 SDS (Simple Dynamic String,简单动态字符串) 数据结构的结构体。 所有结构体都使用了 __attribute__ ((__packed__)),这意味着结构体成员会紧密排列,不进行内存对齐填充,以适应高效的内存管理需求。

内存设计

  • __attribute__ 是关键字,表示这是一个编译器属性, 是 GCC(GNU Compiler Collection)  提供的一个 编译器指令(Compiler Directive) ,用于向编译器传递额外的信息,控制变量、函数、结构体等的编译行为。
  • 括号 ((...)) 内部是具体的属性名称和参数(如 packedalignednoreturn 等)
  • packed 是 __attribute__ 的一个具体属性,用于 取消结构体(struct)或联合体(union)的内存对齐优化,使其成员在内存中 紧密排列(packed) ,不插入任何填充字节(padding)

结构体类型

共有 5 种结构体,分别用于不同长度的字符串:

  • sdshdr5:未实际使用,仅用于文档说明
  • sdshdr8:用于短字符串(长度 ≤ 2^8-1)
  • sdshdr16:用于中等长度字符串(长度 ≤ 2^16-1)
  • sdshdr32:用于长字符串(长度 ≤ 2^32-1)
  • sdshdr64:用于超长字符串(长度 ≤ 2^64-1)
//sds.h
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3

宏定义

//sds.h
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(s) (((unsigned char)(s[-1])) >> SDS_TYPE_BITS)
  • #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));:用于声明并初始化一个指向 SDS 头结构的指针变量 shT 是 SDS 类型(如 8, 16, 32, 64), s 是指向字符串数据的指针, ## 是连接符,将 sdshdr 和 T 连接起来形成结构体名(如 sdshdr8), 计算方式:用字符串指针 s 减去头结构的大小,得到头结构的起始地址,结果:声明了一个 sh 变量,指向 SDS 的头结构
  • #define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))):与第一个类似,但不声明变量,直接返回头结构指针,同样通过字符串指针 s 减去头结构大小来计算头结构地址,结果:返回一个指向 SDS 头结构的指针
  • #define SDS_TYPE_5_LEN(s) (((unsigned char)(s[-1])) >> SDS_TYPE_BITS):专门用于 SDS 类型5(特殊的小字符串优化类型), 它从字符串前一个字节(s[-1])中提取长度信息,SDS_TYPE_BITS 是类型占用的位数(对于类型5是3), 右移操作 >> SDS_TYPE_BITS 是为了去掉低3位的类型信息,只保留长度部分,结果:返回类型5 SDS 字符串的长度

结构体成员

对于实际使用的 sdshdr8 到 sdshdr64,它们包含相同的成员,只是数据类型不同:

  • len:当前字符串的实际长度(已使用的字节数)
  • alloc:分配的总内存大小(不包括头部和空终止符)
  • flags:标志字节
    • 低 3 位表示 SDS 类型(0-4 对应 sdshdr5 到 sdshdr64)
    • 高 5 位未使用(在 sdshdr5 中用于存储长度)
  • buf:柔性数组,实际存储字符串内容(包含空终止符 '\0')

sdshdr5 的特殊性

sdshdr5 没有被实际使用,它的设计是:

  • flags 字节:
    • 低 3 位表示类型(0b000)
    • 高 5 位存储字符串长度(因此最大长度为 31)
  • 没有单独的 len 和 alloc 字段

SDS创建

//sds.h
static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);
        case SDS_TYPE_8:
            return sizeof(struct sdshdr8);
        case SDS_TYPE_16:
            return sizeof(struct sdshdr16);
        case SDS_TYPE_32:
            return sizeof(struct sdshdr32);
        case SDS_TYPE_64:
            return sizeof(struct sdshdr64);
    }
    return 0;
}

//sds.c
char sdsReqType(size_t string_size) {
    if (string_size < 1 << 5) return SDS_TYPE_5;
    if (string_size <= (1 << 8) - sizeof(struct sdshdr8) - 1) return SDS_TYPE_8;
    if (string_size <= (1 << 16) - sizeof(struct sdshdr16) - 1) return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
    if (string_size <= (1ll << 32) - sizeof(struct sdshdr32) - 1) return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}

static inline size_t sdsTypeMaxSize(char type) {
    if (type == SDS_TYPE_5)
        return (1<<5) - 1;
    if (type == SDS_TYPE_8)
        return (1<<8) - 1;
    if (type == SDS_TYPE_16)
        return (1<<16) - 1;
#if (LONG_MAX == LLONG_MAX)
    if (type == SDS_TYPE_32)
        return (1ll<<32) - 1;
#endif
    return -1; /* this is equivalent to the max SDS_TYPE_64 or SDS_TYPE_32 */
}

static inline int adjustTypeIfNeeded(char *type, int *hdrlen, size_t bufsize) {
    size_t usable = bufsize - *hdrlen - 1;
    if (*type != SDS_TYPE_5 && usable > sdsTypeMaxSize(*type)) {
        *type = sdsReqType(usable);
        *hdrlen = sdsHdrSize(*type);
        return 1;
    }
    return 0;
}

/* 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.
 * If SDS_NOINIT is used, the buffer is left uninitialized;
 *
 * The string is always null-terminated (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, int trymalloc) {
    void *sh;

    char type = sdsReqType(initlen);
    /* 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;
    int hdrlen = sdsHdrSize(type);
    size_t bufsize;

    assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
    sh = trymalloc?
        s_trymalloc_usable(hdrlen+initlen+1, &bufsize) :
        s_malloc_usable(hdrlen+initlen+1, &bufsize);
    if (sh == NULL) return NULL;

    adjustTypeIfNeeded(&type, &hdrlen, bufsize);
    return sdsnewplacement(sh, bufsize, type, init, initlen);
}

/* Initializes an SDS within pre-allocated buffer. Like, placement new in C++. 
 * 
 * Parameters:
 * - `buf`    : A pre-allocated buffer for the SDS.
 * - `bufsize`: Total size of the buffer (>= `sdsReqSize(initlen, type)`). Can use 
 *              a larger `bufsize` than required, but usable size won't be greater 
 *              than `sdsTypeMaxSize(type)`. 
 * - `type`   : The SDS type. Can assist `sdsReqType(length)` to compute the type.
 * - `init`   : Initial string to copy, or `SDS_NOINIT` to skip initialization.
 * - `initlen`: Length of the initial string.
 * 
 * Returns:
 * - A pointer to the SDS inside `buf`. 
 */
sds sdsnewplacement(char *buf, size_t bufsize, char type, const char *init, size_t initlen) {
    assert(bufsize >= sdsReqSize(initlen, type));
    int hdrlen = sdsHdrSize(type);
    size_t usable = bufsize - hdrlen - 1;
    sds s = buf + hdrlen;
    unsigned char *fp = ((unsigned char *)s) - 1; /* flags pointer. */

    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            debugAssert(usable <= sdsTypeMaxSize(type));
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            debugAssert(usable <= sdsTypeMaxSize(type));
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            debugAssert(usable <= sdsTypeMaxSize(type));
            sh->alloc = usable;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            debugAssert(usable <= sdsTypeMaxSize(type));
            sh->alloc = usable;
            *fp = type;
            break;
        }
    }
    if (init == SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(s, 0, initlen);
    else if (initlen) 
        memcpy(s, init, initlen);

    s[initlen] = '\0';
    return s;
}

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

Redis 的 SDS 创建过程涉及多个关键函数,它们协同工作以高效分配内存、选择合适的头部类型,并初始化字符串。创建流程通过 类型选择→内存分配→头部初始化→数据填充 的链式调用,在保证高效内存使用的同时,支持动态扩容和二进制安全。核心思想是 用最小的头部开销适配不同长度的字符串,并通过预分配策略优化性能。

  • sdsReqType():根据字符串长度选择 SDS 类型,计算最小能满足长度的头部类型。
  • sdsHdrSize():返回指定类型 SDS 头的字节数,通过 switch-case 匹配类型。
  • sdsTypeMaxSize():返回某类型能存储的最大长度 (1<<N)-1(如 sdshdr8=255)。
  • adjustTypeIfNeeded():检查并调整类型(若预分配空间过大),比较 usable 和类型最大长度决定是否升级 SDS 类型。
  • sdsnewplacement():在预分配内存中初始化 SDS 设置头部字段,拷贝数据,SDS 字符串的实际内存布局为[头部][flags][内容][\0],同时支持三种初始化方式:不初始化、零初始化和从现有数据初始化。
  • _sdsnewlen():主创建函数(处理内存分配和初始化) ,调用上述函数,处理边界条件。
  • sdsnewlen()_sdsnewlen的包装函数,默认使用阻塞分配方式。