Redis 的 SDS 数据结构 | 青训营笔记

68 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天

Redis 中采用了 SDS 作为基本的字符串数据类型,其本质是为更加高效操作动态字符串。本文从 Redis 6.0 源码的角度分析 SDS 数据结构的设计。

首先来看看 C 语言中的 char* 数据结构。

C 语言中的 char*

C 语言的字符串本质上是使用一个空字符 '\0' 结尾的字符数组。在 C 语言标准库中对字符串的操作函数就是通过判断当前字符是否为 '\0' 来决定是否要停止操作。

如获取字符串长度使用的 strlen 函数,就是遍历整个字符串并进行计数。当遇到 '\0' 时停止,并返回统计到的字符个数,即为字符串长度,所以时间复杂度为 O(n),效率并不高。

需要强调的是,C 语言字符串的 '\0' 字符必须出现且只能出现在数组末尾,这就意味着 '\0' 字符不能出现在数组中间。否则字符串操作函数可能会因为接收到 '\0' 字符而提前结束操作,进而发生错误。这无疑给实际生产场景中的使用带来了诸多的限制。如无法存储图片、音频、视频等二进制数据。

其次,C 语言字符串无法记录自身的缓冲区大小,在数据操作时可能会发生缓冲区溢出造成程序提前终止。

Redis 中最基本的数据类型就是字符串,在实际使用中需要频繁对字符串进行修改,而 C 语言字符串存在操作效率低、'\0' 字符带来的限制和可能的缓冲区溢出等问题,所以 Redis 重新设计了一种名为 SDS 的数据结构作为基本的字符串数据类型。

SDS 的基本结构

Redis 中并没有使用传统的 C 语言字符串,而是自定义了一种名为简单动态字符串(simple dynamic string,SDS)的数据结构,并作为包括其他各种自定义数据结构在内使用的用于存储字符串的数据类型。而 C 语言字符串只会作为字符串字面量在无需对其值进行修改的地方(如日志)进行使用。

sds.h 文件中定义了 SDS 数据类型。

typedef char* sds

struct __attribute__ ((__packed__)) sdshdr8 { // 取消在编译过程中的优化对齐

	uint8_t len; // 数据长度
	
	uint8_t alloc; //去掉头和null结束符,有效长度+数据长度

	unsigned char flags; // 低 3 位用于表示类型,高 5 位为保留位没有启用

	char buf[]; //变长数据

};
  • len:字符串长度。这样在获取字符串长度时只需要 O(1)
  • alloc:分配给字符数组的空间长度。在修改字符串时通过计算 alloc - len 可以得到剩余空间大小,判断是否需要扩容,因此不会出现缓冲区溢出问题
  • flags:SDS 的类型。Redis 为了节省内存,针对不同长度的数据采用不同的数据结构,目前总共设计了 5 种(并使用 __attribute__ ((__packed__)) 告诉 GCC 编译器取消编译过程中的优化对齐)
  • buf:实际保存数据的数组,不仅可以保存字符串,也可以保存二进制数据

SDS 采用额外字段来保存字符串长度和内存分配空间,这使得 SDS 的查询操作更加高效,且不会出现缓冲区溢出问题。同时,由于 SDS 不需要通过 '\0' 字符来识别结尾,所以可以存储包含 '\0' 字符的数据。另外,为了兼容部分 C 语言标准库中对于字符串的操作函数,SDS 在数组的结尾还会加上 '\0' 字符。

具体可以参考如下示意图。

SDS 空间扩容

SDS 中的 buf 数组采用 C 语言的动态数组进行定义,并没有指定长度,在修改时如果空间不足需要进行扩容。

扩容的具体操作参照以下规则:

  • 当前有效长度 >= 新增长度,直接返回
  • 更新之后,判断新旧类型是否一致:
    • 如果一致使用 remalloc
    • 不一致则使用 mallocfree

扩容的大小根据以下规则进行决定。

  • 新增后长度小于预分配长度(1024*1024),则扩大一倍;
  • 新增后长度大于等于预分配的长度,每次加预分配长度,这是为了减少不必要的内存
sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;

    //当前有效长度 >= 新增长度,直接返回
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);

    //新增后长度小于预分配长度(1024*1024),扩大一倍;SDS_MAX_PREALLOC=1024*1024
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    //新增后长度大于等于预分配的长度,每次加预分配长度
    else
        newlen += SDS_MAX_PREALLOC;

    type = sdsReqType(newlen);

    // 类型 5 为空类型,不使用
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    
    hdrlen = sdsHdrSize(type);
    if (oldtype==type) {
        // 新老类型一致则使用remalloc
        newsh = s_realloc(sh, hdrlen+newlen+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } else {
        // 不一致则需要重新分配内存
        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);
    }
    sdssetalloc(s, newlen);
    return s;
}