目录

Redis 没有直接使用 C 语言传统的字符串表示, 而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型, 并将 SDS 用作 Redis 的默认字符串表示。

Redis中所有的键都是用sds格式来保存的, 包括一部分值的保存,也是用的sds格式。

SDS的定义

sds.h中的定义如下:

/*
 * 类型别名,用于指向 sdshdr 的 buf 属性
 */
typedef char *sds;

sds包含两部分,在最基本的字符数组之外,加了两个字符数组信息,还有一个类似header的信息,header中包括了一下几个部分:

  • len:表示字符串真正的长度,不包含空终止字符
  • alloc:表示字符串的最大容量,不包含Header和最后的空终止字符
  • flags:表示header的类型

这样做的好处是能够马上知道字符串的长度和剩余空间,而无需遍历一遍字符数组计算长度。

同时,由于在c语言中一般都是通过字符数组最后的”\0”来判断字符串的结束,而sds可以无视这些特殊字符的存在,可以直接根据len来获取完整的字符串。

此外,在修改字符串的时候,sds能够减少空间与分配的次数,提高运行效率,具体的可以稍后看代码中的实现。

SDS基本操作

SDS初始化

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    //根据长度判断创建header的类型
    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;
    //获取header的长度
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */

    //分配空间,header长度+字符串长度+结束字符
    sh = s_malloc(hdrlen+initlen+1);
    if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    //s是字符串真正保存的地址
    s = (char*)sh+hdrlen;
    //fp表示header中的flag
    fp = ((unsigned char*)s)-1;
    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;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
    }
    if (initlen && init)
        //如果有初始内容,复制
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

根据初始化长度的不同,生成的header也不同

/* 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[];
};

len表示已使用的长度, alloc表示分配的长度,包括header的长度和终止符,flag用来标记header的类型。

SDS复制函数

/* Duplicate an sds string. */
//输入sds, 返回复制成功后的地址
sds sdsdup(const sds s) {
    return sdsnewlen(s, sdslen(s));
}

SDS释放函数

/* Free an sds string. No operation is performed if 's' is NULL. */
void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
    //s[-1] 得到header中的flag, 然后通过sdsHdrSize获得header的大小, 相减得到header的地址
    //s_free释放
}

SDS扩容函数

/* 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 sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    // 获取 s 目前的空余空间长度
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;

    // s 目前的空余空间已经足够,无须再进行扩展,直接返回
    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    // s 最少需要的长度
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)
        // 如果新长度小于 SDS_MAX_PREALLOC 
        // 那么为它分配两倍于所需长度的空间
        newlen *= 2;
    else
        // 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
        newlen += SDS_MAX_PREALLOC;

    // 得到新的type类型
    type = sdsReqType(newlen);

    /* Don't use type 5: the user is appending to the string and type 5 is
     * not able to remember empty space, so sdsMakeRoomFor() must be called
     * at every appending operation. */
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;

    hdrlen = sdsHdrSize(type);
    if (oldtype==type) {
        // 如果前后类型不变, 无需更改header,realloc
        newsh = s_realloc(sh, hdrlen+newlen+1);
        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(hdrlen+newlen+1);
        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);
    }
    // 重新设置header中的alloc参数
    sdssetalloc(s, newlen);
    return s;
}
  • 当要增加字符在SDS之后的时候,先判断目前的剩余长度是否满足条件,如果慢煮条件,直接将字符串添加在后边
  • 如果当前的剩余空间不够,需要对SDS进行扩容。如果对 SDS 进行修改之后, SDS 的长度(也即是 len 属性的值)将小于 1 MB , 那么程序分配和 len 属性同样大小的未使用空间, 这时 SDS len 属性的值将和 free 属性的值相同。 举个例子, 如果进行修改之后, SDS 的 len 将变成 13 字节, 那么程序也会分配 13 字节的未使用空间, SDS 的 buf 数组的实际长度将变成 13 + 13 + 1 = 27 字节(额外的一字节用于保存空字符)。
  • 如果对 SDS 进行修改之后, SDS 的长度将大于等于 1 MB , 那么程序会分配 1 MB 的未使用空间。 比如, 如果进行修改之后, SDS 的 len 将变成 30 MB , 那么程序会分配 1 MB 的未使用空间, SDS 的 buf 数组的实际长度将为 30 MB + 1 MB + 1 byte 。

SDS小结

SDS提供了一系列函数,不一一列出。 参考《redis设计与实现》一书的说明,SDS与C字符串的区别如下

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