c++数据结构代码整理_redis 底层数据结构(1)——sds

98c2536b5c4f70e23a2bbd90d226f8aa.png

从 redis 内部实现的角度来说,redis 提供了 sds, dict, skiplist,  ziplist 和 quicklist 数据结构,初步计划分为以下系列文章对 redis 数据结构进行归纳总结:

redis 底层数据结构(1)——sds

redis 底层数据结构(2)——dict

redis 底层数据结构(3)——skiplist

redis 底层数据结构(4)——ziplist

redis 底层数据结构(5)——quicklist

redis 底层数据结构(6)——intset

从使用者的角度来说,redis 提供了 string(字符串),list(列表),hash(哈希),set(集合)和 sorted set(有序集合)五种数据类型,这些将在 redis 对象系列文章进行归纳学习。

Redis 暴露给用户的字符串数据类型(string),在其内部使用 SDS 数据结构来存储字符串变量的。本文基于 redis-6.0.5 整理,完成时间:2020.06.13。

1 SDS 的数据结构

存储字符串变量时,Redis 没有使用 C 语言的字符串表示方法——以空字符'\0'结尾的字符数组,而是定义了 SDS(Simple Dynamic String,简单动态字符串)的抽象类型,并将 SDS 作为 Redis 的默认字符串表示。

当存储的是一个可以被修改的字符串值时,Redis 就会使用 SDS 来表示字符串值,在 Redis 的数据库里,包含字符串值的键值对在底层都是由 SDS 实现的,同时采用预分配冗余空间的方式来减少内存的频繁分配。

以下分析基于 redis 的存储的值是字符串类型,当存储的是整型数据时,数据结构不再是 sds 了,需单独分析。

【源码】

// redis-6.0.5/src/sds.htypedef char *sds;/* 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 个常量表示 5 种 sds struct 类型,即 flags 的有效取值范围// sds struct 类型: sdshdr5, sdshdr8, sdshdr16, sdshdr32, sdshdr64#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

【解析】:

  • 计算机及 C 语言基础:

    • msb: Most Significant Bit, 最高有效位;lsb: Least Significant Bit, 最低有效位

    • 使用 __attribute__ ((__packed__)) 声明结构体的作用:让编译器以紧凑模式来分配内存,如果不声明该属性,编译器可能会为结构体成员做内存对齐优化,在其中填充空字符。这样就不能保证 结构体成员在内存上是紧凑相邻的,也不能通过 buf 向低地址偏移一个字节来获取 flags 字段的值

    • char buf[]:一个没有指定长度的字符数组,这是 c 语言定义字符数组的一种特殊写法,称为 flexible array member,只能定义在一个结构体的最后一个成员。buf 起标记作用,表示在 flags 字段后面就是一个字符数组,程序在为 sdshdr 分配内存时,它并不占用内存空间

    • sizeof(struct sdshdr5) == 1; sizeof(struct sdshdr8) == 3; sizeof(struct sdshdr16) == 5; sizeof(struct sdshdr32) == 9;  sizeof(struct sdshdr64) == 17

  • sdshdr5 的成员分析:

    • flags:3 lsb of type, and 5 msb of string length,低 3 位用于表示 sds struct 的类型,即使用哪种 sdshdr,5 位高有效位用于记录字符串的长度。(数字 5 很重要,后续字会以 2^5 作为字符串长度的分界线,从而决定选择哪种 sdshdr)

    • buf: 存储字符串内容的字符数组,其长度等于 alloc+1。一般情况下,buf 字符数组的最大容量大于字符串的真实长度,未使用的字符数组空间一般用'\0'字符 填充,这样可实现在不重新分配内存的前提下实现字符串扩展。为了兼容 C字符串,在真正字符串之后,还有一个  NULL 结束符(即 ASCII 码为 0 的 '\0' 字符),所以 buf 的长度比最大容量多一个字节。而多出来的这个字节空间正是为了在字符串长度达到最大容量时,仍有足够的空间存放'\0'字符。

  • sdshdr8, sdshdr16, sdshdr32, sdshdr64 的成员分析:

    • len:表示字符串的实际长度,不包含 C 字符串结尾的 '\0' 字符

    • alloc:字符串数组 buf 的最大容量,不包含 len, alloc, flag 和 buf 的结束符(即 '\0')

    • flags:占用 1 个字节的内存空间,低三位表示 sdshdr 的类型,另外 5 个比特位暂未使用

    • buf:与 sdshdr 的 buf 一致

由上述分析可知,不同长度的字符串可以使用不同的类型的 sdshdr,短字符串使用较小的 struct,从而节省内存。

备注:“sdshdr5 is never used, we just access the flags byte directly. ”查看 redis 源码发现 sdshdr5 是有用的,所以个人觉得这句注释不对。

2 图解 SDS

C 语言同一个结构体里的成员变量,在内存上的地址是连续的。

以下讲解字符串 "Redis" 分别使用 sdshdr8 和 sdshdr16 结构体的存储示意图。

2.1 sdshdr8

首先分析 sdshdr8 的结构体成员:

  • len 和 alloc 都是 uint8_t 类型(typedef unsigned char uint8_t;),也就是 len 和 alloc 都是占用 1 个字节的内存空间

  • flags 是 unsigned char 类型,占用 1 个字节的内存空间。由 #define SDS_TYPE_8  1 可知,此时 flags 的值为 1

假设存储字符串为"Redis",由于字符串 "Redis" 的大小为 5,所以 len 为 5;再假设 alloc 为 9,所以 buf 字符数组的大小为 10。

由上述分析可得"Redis"在内存的存储如下图所示:

d86bd2f6f514cef97cd33cdb7c6f007b.png

以下代码用于验证上述内存示例地址的合理性

#include typedef unsigned char uint8_t;struct 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[];};int main() {    struct sdshdr8 sds;    printf("%p\n%p\n%p\n%p\n", &sds, &sds.len, &sds.alloc, &sds.flags);    return 0;}/* 运行环境:macOS 10.14.6 Intel Core i5输出内容:0x7ffeec3a98980x7ffeec3a98980x7ffeec3a98990x7ffeec3a989a*/

2.2 sdshdr16

首先分析 sdshdr16 的结构体成员:

  • len 和 alloc 都是 uint16_t 类型(typedef unsigned short int uint16_t;),也就是 len 和 alloc 都是占用 2 个字节的内存空间

  • flags 是 unsigned char 类型,占用 1 个字节的内存空间,由 #define SDS_TYPE_16 2 可知,此时 flags 的值为 2

假设存储字符串为"Redis",由于字符串 "Redis" 的大小为 5,所以 len 为 5;再假设 alloc 为 11,所以 buf 字符数组的大小为 12。

由上述分析可得"Redis"在内存的存储如下图所示:

628485168c8183c8c806d95d59a9e254.png

以下代码用于验证上述内存示例地址的合理性。

#include typedef unsigned short int uint16_t;struct 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[];};int main() {    struct sdshdr16 sds;    printf("%p\n%p\n%p\n%p\n", &sds, &sds.len, &sds.alloc, &sds.flags);    return 0;}/* 运行环境:macOS 10.14.6 Intel Core i5输出内容:0x7ffee662d8900x7ffee662d8900x7ffee662d8920x7ffee662d894*/

sdshdr32 和 sdshdr64 的存储方式与上述类似,不再重复分析;sdshdr5 的存储方式比较特殊,字符串的长度存储在 flags 字段的低 5 位有效位中,由于没有 alloc 字段,所以无法实现内存预分配的功能。

小结:SDS 字符串的 header 部分(由 len, alloc 和 flags 组成)其实是隐藏在真正字符串的前面,也就是内存低地址方向,好处:减少内存碎片,提高存储效率。虽然 sds 有多个结构体,但是都可以同 char* 来表示,当 SDS 存储的是可打印字符串,此时可以直接将它赋值给 C 函数,比如使用 printf() 函数打印等。

3 SDS 的主要函数

3.1 创建字符串

【函数原型】

sds sdsnew(const char *init);

【功能】

创建新的 sds 字符串。

【源码】

// redis-6.0.5/src/sds.h#define SDS_TYPE_BITS 3sds sdsnew(const char *init);// redis-6.0.5/src/sds.cstatic inline char sdsReqType(size_t string_size) {    if (string_size < 1<<5)        return SDS_TYPE_5;    if (string_size < 1<<8)        return SDS_TYPE_8;    if (string_size < 1<<16)        return SDS_TYPE_16;#if (LONG_MAX == LLONG_MAX)    if (string_size < 1ll<<32) // ll 代表 long long, 所以 1ll 表示 long long 类型的数字 1        return SDS_TYPE_32;    return SDS_TYPE_64;#else    return SDS_TYPE_32;#endif}sds sdsnewlen(const void *init, size_t initlen) {    void *sh;    sds s;    char type = sdsReqType(initlen); // 判断使用哪种 sdshdr 结构体    /* 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); // 计算 sdshdr header 占用多少字节的内存    unsigned char *fp; /* flags pointer. */    // 只有一次内存分配,包含:header,数据,额外一个空字节(用于兼容 C 字符串)    sh = s_malloc(hdrlen+initlen+1);    if (init==SDS_NOINIT)        init = NULL;    else if (!init)        memset(sh, 0, hdrlen+initlen+1);    if (sh == NULL) return NULL;    s = (char*)sh+hdrlen; // 指针偏移 hdrlen 字节,此时 s 指向有效字符数组的起始位置    fp = ((unsigned char*)s)-1; // fp 指针向低地址偏移 1 字节,此时 fp 指向 flags 成员    switch(type) {        case SDS_TYPE_5: { // 将字符串的长度存储在 flags 成员的高 5 位有效位中            *fp = type | (initlen << SDS_TYPE_BITS);            break;        }        case SDS_TYPE_8: {            SDS_HDR_VAR(8,s);            sh->len = initlen; // 实现获取字符串长度的时间复杂度为 O(1)             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;}/* Create a new sds string starting from a null terminated C string. */sds sdsnew(const char *init) {    size_t initlen = (init == NULL) ? 0 : strlen(init);    return sdsnewlen(init, initlen);}

【解析】

  • 字符串长度范围 [0, 2^5):sdshdr 的类型为 SDS_TYPE_5,即使用 sdshdr5,由 sdsnewlen 函数可知,当字符串长度为 0 时,会使用 sdshdr8

  • 字符串长度范围 [2^5, 2^8):sdshdr 的类型为 SDS_TYPE_8,即使用 sdshdr8

  • 字符串长度范围 [2^8, 2^16):sdshdr 的类型为 SDS_TYPE_16,即使用 sdshdr16

  • 字符串长度范围 [2^16, 2^32):sdshdr 的类型为 SDS_TYPE_32,即使用 sdshdr32

  • 字符串长度范围 [2^32, 2^64):sdshdr 的类型为 SDS_TYPE_64,即使用 sdshdr64

  • Empty strings are usually created in order to append. Use type 8 since type 5 is not good at this. 创建长度为 0 的空字符串时,不使用 sdshdr5,而是使用 sdshdr8,这是因为 sdshdr8 方便实现字符串追加操作,而 sdshdr5 并不适合,需要重新分配内存

  • s[initlen] = '\0'; 初始化的 SDS 字符串,数据后会追加一个空字符'\0',目的是为了兼容 C 字符串

3.2 删除字符串

【函数原型】

void sdsfree(sds s);

【功能】

释放内存,将内存归还给系统。

【源码】

// redis-6.0.5/src/sds.h#define SDS_TYPE_MASK 7 // 二进制表示 00000111void sdsfree(sds s);// redis-6.0.5/src/sds.c// sdsHdrSize 函数返回不同 sdshdr 类型的 header 部分的长度static inline int sdsHdrSize(char type) {    switch(type&SDS_TYPE_MASK) { // type 与 7 进行按位与运算,最终只保留低 3 位有效位(sdshdr5 的字符串长度存在 type 的高 5 位有效位中)        case SDS_TYPE_5:            return sizeof(struct sdshdr5); // sdshdr5 header 的长度 1        case SDS_TYPE_8:            return sizeof(struct sdshdr8); // sdshdr8 header 的长度 3        case SDS_TYPE_16:            return sizeof(struct sdshdr16); // sdshdr16 header 的长度 5        case SDS_TYPE_32:            return sizeof(struct sdshdr32); // sdshdr32 header 的长度 9         case SDS_TYPE_64:            return sizeof(struct sdshdr64); // sdshdr64 header 的长度 17    }    return 0;}/* 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] 通过字符串内存向低地址便宜一个字节,获取 flags 的值

  • sdsHdrSize(s[-1])) 计算得出不同 sdshdr 的 header 所占的字节数

  • (char*)s-sdsHdrSize(s[-1]) 将 s 指针向低地址偏移 header 的长度, 找到分配内存的起始地址

  • 通过调用 s_free 函数释放内存

3.3 连接字符串

【函数原型】

sds sdscat(sds s, const char *t);

【功能】

sdscat 将 t 指针指向的任意二进制数据追加到 sds 字符串 s 的后面。

【源码】

// redis-6.0.5/src/sds.hsds sdscatlen(sds s, const void *t, size_t len);sds sdscat(sds s, const char *t);#define SDS_TYPE_MASK 7 // 二进制表示为 00000111// 将 s 指针向低地址便宜 header 大小的字节数,可得 sds 字符串 s 的起始指针位置#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));#define SDS_MAX_PREALLOC (1024*1024) // 单位: byte,所以 sds 字符串最大预分配内存大小为 1M 字节// sdsavail 获取 sds 字符串 s 的可用内存空间,单位: byte(字节)static inline size_t sdsavail(const sds s) {    // s 指向 sds 字符串的 buf, 即真正字符串的内存地址    unsigned char flags = s[-1]; // s[-1] 往 s 指针的低地址方向偏移一个字节, 即指向 flags 字段    switch(flags&SDS_TYPE_MASK) { // 将 flags 变量的高 5 位有效位置为 0        case SDS_TYPE_5: { // sdshdr5 无预申请内存空间,所以直接返回 0,这也说明 sdshdr5 不适合字符串追加操作            return 0;        }        case SDS_TYPE_8: {            SDS_HDR_VAR(8,s);            return sh->alloc - sh->len; // 字符数组 buf 的最大长度减去已使用字节数,可得字符数组的可用空间        }        case SDS_TYPE_16: {            SDS_HDR_VAR(16,s);            return sh->alloc - sh->len;        }        case SDS_TYPE_32: {            SDS_HDR_VAR(32,s);            return sh->alloc - sh->len;        }        case SDS_TYPE_64: {            SDS_HDR_VAR(64,s);            return sh->alloc - sh->len;        }    }    return 0;}// 设置 sds 字符串 s 的 len 字段为 newlenstatic inline void sdssetlen(sds s, size_t newlen) {    unsigned char flags = s[-1];    switch(flags&SDS_TYPE_MASK) {        case SDS_TYPE_5:            {                unsigned char *fp = ((unsigned char*)s)-1;                *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);            }            break;        case SDS_TYPE_8:            SDS_HDR(8,s)->len = newlen;            break;        case SDS_TYPE_16:            SDS_HDR(16,s)->len = newlen;            break;        case SDS_TYPE_32:            SDS_HDR(32,s)->len = newlen;            break;        case SDS_TYPE_64:            SDS_HDR(64,s)->len = newlen;            break;    }}// 获取 sds 字符串 s 的有效字符串长度,若存入 redis 的字符串为"hello",则长度为 5static inline size_t sdslen(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)->len;        case SDS_TYPE_16:            return SDS_HDR(16,s)->len;        case SDS_TYPE_32:            return SDS_HDR(32,s)->len;        case SDS_TYPE_64:            return SDS_HDR(64,s)->len;    }    return 0;}// 设置 sds 字符串 s 的 alloc 字段的值为 newlen,即设置字符数组的最大容量static inline void sdssetalloc(sds s, size_t newlen) {    unsigned char flags = s[-1];    switch(flags&SDS_TYPE_MASK) {        case SDS_TYPE_5:            /* Nothing to do, this type has no total allocation info. */            break;        case SDS_TYPE_8:            SDS_HDR(8,s)->alloc = newlen;            break;        case SDS_TYPE_16:            SDS_HDR(16,s)->alloc = newlen;            break;        case SDS_TYPE_32:            SDS_HDR(32,s)->alloc = newlen;            break;        case SDS_TYPE_64:            SDS_HDR(64,s)->alloc = newlen;            break;    }}// redis-6.0.5/src/sds.c/* 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;    size_t avail = sdsavail(s);    size_t len, newlen;    char type, oldtype = s[-1] & SDS_TYPE_MASK;    int hdrlen;    /* Return ASAP if there is enough space left. */    if (avail >= addlen) return s; // 可用空间够用,返回原有 sds 字符串 s    // 需要重新分配内存    len = sdslen(s);    sh = (char*)s-sdsHdrSize(oldtype); // sh 指针指向 sds 字符串 s 的 header 的起始位置    newlen = (len+addlen); // 新的长度等于原有字符串长度加上待追加的字符串长度    if (newlen < SDS_MAX_PREALLOC) // 如果新的字符串长度小于 1M 字节        newlen *= 2; // 新的长度翻倍,即冗余空间为 newlen 字节    else        newlen += SDS_MAX_PREALLOC; // 新的长度再增加 SDS_MAX_PREALLOC,即冗余空间为 1M 字节    type = sdsReqType(newlen); // 重新选择 sdshdr 类型    /* 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; // 若是 sdshdr5 则使用 sdshdr8,减少重新分配内存的次数    hdrlen = sdsHdrSize(type);    if (oldtype==type) { // 新的 sdshdr 类型与原有的相同,即 hedaer 未发生变化        // s_realloc 尝试在原有内存地址上重新分配空间,如果原来的内存为止有足够的空间完成重新分配,        // 则返回的新地址与传入的旧地址相同;反之,该函数将分配新的内存块,并完成数据迁移。        newsh = s_realloc(sh, hdrlen+newlen+1);        if (newsh == NULL) return NULL;        s = (char*)newsh+hdrlen;    } else { // 新的 sdshdr 类型与原有的不同,即 hedaer 发生变化        // 整个字符串空间(包括 header)都需要重新分配,并拷贝。        /* 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); // 释放原有 sds 的内存空间        s = (char*)newsh+hdrlen; // 让 s 指针指向新的 sds 的内存起始位置        s[-1] = type;        sdssetlen(s, len);    }    sdssetalloc(s, newlen);    return s;}/* 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 termianted 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));}

【解析】

  • redis 的 append 命令,在 redis 内部由 sdscat 函数完成

  • 通过 sdsMakeRoomFor 函数保证 sds 字符串 s 有足够的空间来保存追加的字符串,可能会重新分配内存也可能不会,取决于追加的字符串长度和 sds buf 数组的剩余空间大小

4 SDS 与 C 字符串的区别

  • 获取字符串长度的时间复杂度:SDS 只需O(1),而 C 字符串需要 O(n)

  • 可动态扩展内存,SDS 标识的字符串内容可以修改,也可以追加。申请的内存一般大于字符串的大小,可减少修改字符串时带来的内存重分配次数

  • SDS 是二进制安全(binary safe)的,能存储任意二进制数据,而不仅仅是可打印字符。而 C 字符串以 '\0' 字符标识字符串结束,无法存 '\0' 字符

  • SDS 与 C 字符串类型兼容,且兼容部分 C 字符串函数

5 参考资料

  • redis-6.0.5 源码(http://download.redis.io/releases/redis-6.0.5.tar.gz

  • http://zhangtielei.com/posts/blog-redis-sds.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值