Redis 源码分析简单字符串 (sds)

540 阅读4分钟

「这是我参与2022首次更文挑战的第29天,活动详情查看:2022首次更文挑战」。

SDS 是简单动态字符串(Simple Dynamic String)的缩写,也是 Redis 设计实现的基础。

简单说明:本文主要是基于redis 源码的 unstable 分支说明,后续 Redis 设计相关文章不再特别说明。

数据结构

redis 为了节省内存,针对不同的长度数据采用不同的数据结构。
sds.h 中定义了如下共五种,通常 SDS_TYPE_5 并不使用,因为该类型不会存放数据长度,每次都需要进行分配和释放。

#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

以 type = 1 为例

typedef char *sds;

/*
__attribute__ ((__packed__)) 的所用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用
 */
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 数据长度 */
    uint8_t alloc; /* 去掉头和 null 结束符,有效长度+数据长度 */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    // 变长数据
    char buf[];
};

展示一个数据例子:

image-20220213101257706.png

  • free 属性的值为 0 表示这个 sds 没有分配任何未使用空间。
  • len 属性的值为 5 , 表示这个 sds 保存了一个 5 个长度的字符串。
  • buf 属性是一个 char 类型的数组,数组保存了前面5个字节的字符串 'R'、'e'、'd' 、'i'、's' 。最后一个字符则保存了空字符 '\0'。

对于数据压缩的演示:

image-20220213111230228.png

空间拓容

  • 当前有效长度 >= 新增长度,直接返回;

  • 更新之后,判断新旧类型是否一致;

    • 一致使用 remailc, 否则使用 malloc + free

      • a. 当前有效长度 >= 新增长度,直接返回
  • 增加步长

    • 新增长度小于预分配长度(1024 * 1204),扩大一倍
    • 新增长度大于等于预分配的长度,每次加预分配长度(减少不必要的内存)

文件位置: sds.c/_sdsMakeRoomFor

#define SDS_MAX_PREALLOC (1024*1024)

sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);
    size_t len, newlen, reqlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    size_t usable;

    /* Return ASAP if there is enough space left. */
    //当前有效长度 >= 新增长度,直接返回
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    reqlen = newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    if (greedy == 1) {
        //新增后长度小于预分配长度(1024 * 1024,扩大一倍: SDS_MAX_PREALLOC = 1024 * 1024)
        if (newlen < SDS_MAX_PREALLOC)
            newlen *= 2;
       // 新增后长度大约等于预分配的长度,每次增加预分配长度
        else
            newlen += SDS_MAX_PREALLOC;
    }

    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;
    
    // 新老类型一致使用 remalloc, 否则使用 malloc + freea , 当前有效长度 >= 新增长度,直接返回
    hdrlen = sdsHdrSize(type);
    assert(hdrlen + newlen + 1 > reqlen);  /* Catch size_t overflow */
    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);
    }
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        usable = sdsTypeMaxSize(type);
    sdssetalloc(s, usable);
    return s;
}

空间缩容

在 trim 操作时,采用的是惰性释放即:不会立即使用内存重新分配来回收缩短的字节,只是移动和标记,并修改数据。

文件位置 sds.c/sdstrim

sds sdstrim(sds s, const char *cset) {
    char *end, *sp, *ep;
    size_t len;

    sp = s;
    ep = end = s+sdslen(s)-1;
    while(sp <= end && strchr(cset, *sp)) sp++;
    while(ep > sp && strchr(cset, *ep)) ep--;
    len = (ep-sp)+1;
    if (s != sp) memmove(s, sp, len);
    s[len] = '\0';
    sdssetlen(s,len);
    return s;
}

真正的删除操作是在后续操作中见 tryObjectEncoding

文件位置 server.c/tryObjectEncoding

void trimStringObjectIfNeeded(robj *o) {
    if (o->encoding == OBJ_ENCODING_RAW &&
        sdsavail(o->ptr) > sdslen(o->ptr)/10)
    {
        o->ptr = sdsRemoveFreeSpace(o->ptr);
    }
}

优点

  • 常量获取字符串长度(len)
  • 避免缓冲区溢出
  • 减少字符串修改带来的内存频繁重分配次数
  • 二进制操作安全:可以保持文本数据,也可以保持任意格式的二机制数据(如视频流等)
  • 以 '\0' 结尾,使其兼容部分 C 字符串函数

其他

  • sds 是 char* 的别名,可以理解为分配的是一块连续的内存(表头 + 数据),根据局部性原理可以提高访问速度。
  • 数据存储不使用 SDS_TYPE_5, 因为该类型每次新数据时,都需要进行拓充。
  • 利用 C 语言内存布局,在 SDS 结构体中使用了一个 0 长度的数组,既可以达到变长,又能保证内存也是连续的,因此在 SDS 一系列操作中,看到使用 s[-1] 这样的操作搞的很惊讶,当然这里 s 指向的是 buf 位置。

文件位置:sds.c/sdsalloc

/* sdsalloc() = sdsavail() + sdslen() */
static inline size_t sdsalloc(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
            return SDS_HDR(8,s)->alloc;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->alloc;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->alloc;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->alloc;
    }
    return 0;
}

参考资料