【Redis源码】Redis 6.0 字符串 SDS 源码分析

Redis 6.0 字符串 SDS 源码分析


如果觉得对你有帮助,能否点个赞或关个注,以示鼓励笔者呢?!博客目录 | 先点这里

  • 前提概念
    • 源码
    • 为什么要自己重设计字符串?
    • Redis SDS & C 语言字符串
  • 源码分析
    • 结构设计
    • 基本概念
    • 创建 SDS
    • SDS 扩容
    • SDS 缩容

前提概念


源码

  • 基于 Redis 6.0 版本
  • src/sds.h
  • src/sds.c

为什么要自己重设计字符串?

说白了就是 C 语言中的字符串不能满足 Redis 的需要,所以需要对其进行一定的改造


Redis SDS & C 语言字符串

从 《Redis设计与实现》这本书中,我们可以知道 Redis 的 SDS (Simple Dynamic String)与 C 字符串有如下区别

DIFF SDS C 字符串
获取字符串长度的时间复杂度 O(1) O(n)
字符串函数是否会造成缓冲区溢出
每次修改字符串需要重新分配内存
二进制安全 是,可以保存文本,以及其他二进制 (图片等) 数据 否,只能存储文本
函数重用 能使用 库部分函数 自然可以使用 库全部函数

说白了,最重要的特性就是

  • SDS 动态长度,留有一定冗余,减少定长字符串频繁分配空间的消耗,提高性能
  • 有专属的成员记录长度,无需 O(n) 遍历
  • 二进制安全

源码分析


结构设计

SDS 定义

typedef char *sds;
  • redis 没有直接使用 sdshdr 结构,而是定义了sds类型来操作sdshdr结构

3.2 之前的 SDS 结构体,《Redis 设计与实现》 就是基于这个版本的 SDS 源码讲解的

struct sdshdr {
     
	// 记录 buf 数组中已经使用字节的数量
	// 等于 SDS 所保存字符串的长度
    unsigned int len;
    // 记录 buf 数组中还未使用字节的数量
    unsigned int free;
    // 字节数组,数据域,保存字符数据
    char buf[];
};

【Redis源码】Redis 6.0 字符串 SDS 源码分析_第1张图片

  • 每个 src/sds.h/sdshdr 都表示一个 SDS 值
  • 假设 free 值 = 0, 代表这个 SDS 没有分配或没有剩下任何未使用的空间
  • 假设 len = 10 ,代表这个 SDS 保留了一个 5 byte 长度的字符串
  • redis 3.2 前的源码更方便咱们理解和学习,但 3.2 之后的优化后会稍微复杂,图示会有些不一样

3.2 之后的 SDS 结构体, 相比以前稍微复杂,其根据字符串的长度,划分了 5 种结构体

  • sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64
/* 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 {
     
	// 低三位存储类型,其余 5 位存储长度
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
     
	// 字符串在 buf 中实际占用的字节数 (不包含结束符 '\0')
    uint8_t len; /* used */
    // buf 去掉去除头长度和结束符的总长度,即 head + end + len + free
    uint8_t alloc; /* excluding the header and null terminator */
    // 低位 3 bit 表示结构类型,其余 5 位未使用
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    // 数据域,与 3.2 之前版本一致
    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 [0, 2^5) 31
8 [2^5, 2^8) 2^8-1
16 [2^8, 2^16) 2^16-1
32 [2^16, 2^32) 2^32 -1
64 [2^32, 2^64) 2^64 -1
  • 除了 sdshdr5 的结构稍显不一致,其余类似
    • len 表示 SDS 已使用的长度,与 3.2 版本之前一致 (不包含 ‘\0’)
    • alloc 表示 SDS 的最大容量,即 buf 真实可用于存储字符串的空间 (buf 的真实分配大小 - 头尾空间),略小于 buf 字节数组的长度
      • 剩余空间 free = alloc - len
    • flags 表示字符串类型,占用 1 个字节,低 3 位表示 header 的类型
      • SDS_TYPE_5,SDS_TYPE_8,SDS_TYPE_16,SDS_TYPE_32,SDS_TYPE_64
    • buf 是柔性数组 (flexible array member)

基本概念

一、attribute ((packed))

毕竟 C 太弱,所以直接贴别人的理解,似乎挺重要的

  • __attribute__ ((__packed__)) 是C语言的一种关键字,这将使这个结构体在内存中不再遵守字符串对齐规则,而是以内存紧凑的方式排列。目的时在指针寻址的时候,可以直接通过 sds[-1] 找到对应 flags,有了flags就可以知道头部的类型,进而获取到对应的len,alloc信息
  • packed 或者 attribute__((packed)) 关键字的作用就是用来打包数据的时候以 1 来对齐,比如说用来修饰结构体或者联合体的时候,那么这些成员之间就没有间隙(gaps)了。如果没有加,那么这样结构体或者联合体就会以他的自然对齐方式来对齐。比如某 CPU 架构的编译器默认对齐方式是4, int 的 size 也是 4,char 的 size 是 1,那么类似
typedef __packed struck test_s {
     
	char a;
	int b;
}	test_t;
  • 这样定义结构体的 size 就是 8 个字节 (4 字节对齐的情况下)。如果加上 packed ,size 就会变成 5 个字节,中间是没有 gaps 的

  • 这个概念很重要,redis 源码中不是直接对 sdshdr 某一个类型操作,往往参数都是sds,而 sds 就是结构体中的 buf,在后面的源码分析中,你可能会经常看见 s[-1] 这种魔法一般的操作,而按照 sdshdr 内存分布 s[-1] 就是sdshdr中flags变量,由此可以获取到该 sds 指向的字符串的类型

  • redis源码解析-字符串 - @静夜

二、 SDS_TYPE 定义
#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 // 2^3 - 1
#define SDS_TYPE_BITS 3
  • SDS_TYPE 使用 flags & SDS_TYPE_MASK 来获取动态字符串对于的字符串类型, 说白了就是 flags % 2^3
  • 好奇 flags & SDS_TYPE_MASK = 5,6,7 时,会是什么?
三、SDS 的长度
#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(f) ((f)>>SDS_TYPE_BITS)

  • SDS_HDR 函数通过是 结构体的首地址 = (sds 的字节数组) - (对应结构体定义的 size) 获得对于 sds 的引用 (按 Java 的话说)
static inline size_t sdslen(const sds s) {
     
	// 通过 s[-1] 获得到该 sds 的 flags
    unsigned char flags = s[-1];
    // 获得指定的
    switch(flags&SDS_TYPE_MASK) {
     
        case SDS_TYPE_5:
        	// 获取 SDS_TYPE_BITS = 3
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8:
        	// 通过 SDS_HDR 获得结构体首地址
        	// 通过 len 获得对于大小
            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;
}
  • sdslen 用于获得 sds 已使用字符串长度
static inline size_t sdsavail(const sds s) {
     
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
     
        case SDS_TYPE_5: {
     
        	// 5 没有多余空间,适合静态字符串,需要扩容需要重新分配
            return 0;
        }
        case SDS_TYPE_8: {
     
        	// 获得 sh, 使得 sh 可以访问 alloc 和 len
        	// 剩余可用空间 free = alloc - len
            SDS_HDR_VAR(8,s);
            return sh->alloc - sh->len;
        }
        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 剩余可用长度, free = alloc - len

我们知道传统 C 语言要想知道字符串的长度,需要 O(n) 的时间复杂度去遍历长度,直到找到结束符 \0。 SDS 是不需要的,只需要得到结构体的 len 即可。

但我们看到方法参数中的 sds, 即 s, 并不是某个 sdshdr 结构体的引用,而是该结构体的字节数组。换成 Java 的话就是,我没有拿到你的对象,你给我的是这个对象变成字节数组的形态,我无法通过 object.len 的方法获得 len,那怎么办呢?看下面代码

SDS_HDR(8,s);
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
// 翻译后
((struct sdshdr8 *)((s) - (sizeof(struct sdshdr8))))

结构体的首地址 = (sds 的字节数组) - (对应结构体定义的 size)

  • 换成 Java 的话来说,我终于拿到了这个 sds 对象的引用了!
  • sdshdr8 在取消内存对齐后的结构体定义 size 是 3 个字节, uint8_t len, uint8_t alloc, flags 各占用 1 个字节,buf 是柔性数组,初始值为 0,不占用空间
  • 至于为什么可以,就要去更深入了解 c 了,java 系看着还是挺耗时间的,ROI 问题,就不纠结了

总之然后我就可以通过该引用获得 len 的大小


创建 SDS

/* 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);
}
/* 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-termined (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
 * 参数:
 * 1. *init    初始化内容
 * 2. initlen  初始大小
 */
sds sdsnewlen(const void *init, size_t initlen) {
     
    void *sh;
    sds s;
    // 根据传入的初始大小获得 sds 类型 (SDS_TYPE)
    char type = sdsReqType(initlen);
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
	// 因为字符串都会追加操作,所以最低使用 sdshdr8 类型
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    // 获得对于的结构体长度
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */
	
	// 分配内存
    assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
    sh = s_malloc(hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
	
	// 根据 SDS_TYPE 来初始 sds  内容
    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;
        }
    }
    // 新 sds 初始完后,将 init 内容拷贝到 sds 中
    if (initlen && init)
        memcpy(s, init, initlen);
    // sds 末尾置为结束符    
    s[initlen] = '\0';
    return s;
}

  • 根据传入的初始化字符串长度,选择 SDS_TYPE, 最低是 SDS_TYPE_8
  • SDS 内存分配公式 size = hdrlen + initlen + 1
    • 假设 type = sds_type_8, initlen = 5, 那么 size = 3 + 5 +1 = 9 字节
    • 所以可以 SDS 空间 = len + alloc + buf + 1 字节的\0

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 扩容 addlen 大小
 * 参数:
 * 1. s 对于 sds
 * 2. addlen 至少要多满足的长度
 * */
sds sdsMakeRoomFor(sds s, size_t addlen) {
     
    void *sh, *newsh;
    // 获取当前 sds 的剩余空间
    size_t avail = sdsavail(s);
    size_t len, newlen;
    // 获得 SDS_TYPE
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
	
	// 如果 SDS 剩余空间足够,就无需扩容了
    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;
	
	// 当前 sds len 大小
    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    // 扩容后的新大小 = len + addlen, 以及后续对 newlen 的处理
    newlen = (len+addlen);
    assert(newlen > len);   /* Catch size_t overflow */
    // newlen 长度处理
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;

	// 根据新长度,获得新的 SDS_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;
	
	// 获得新 type 的 hdrlen
    hdrlen = sdsHdrSize(type);
    assert(hdrlen + newlen + 1 > len);  /* Catch size_t overflow */
    // 如果旧类型和新类型一致,则直接对旧结构体分配更多内存空间即可
    if (oldtype==type) {
     
        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;
        // 更新 sds 已使用空间长度,即更新 len
        sdssetlen(s, len);
    }
    // 更新 alloc
    sdssetalloc(s, newlen);
    return s;
}
  • 扩容倍数规则
    1. newlen = len + addlen
    2. 如果 newlen 小于 1M,则 newlen *= 2, 两倍扩容
    3. 如果 newlen 大于 1M, 则 newlen += 1M, 每次扩容 1M

SDS 缩容

SDS 在缩容的时候,并非立即释放内存,而已通过调整 len 的大小,逻辑缩容,并非物理缩容。即通过调整 len ,告诉外界 len 外的数据废弃了,当做是冗余空间。比如 sdstrim 函数

/* Remove the part of the string from left and from right composed just of
 * contiguous characters found in 'cset', that is a null terminted C string.
 *
 * After the call, the modified sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call.
 *
 * Example:
 *
 * s = sdsnew("AA...AA.a.aa.aHelloWorld     :::");
 * s = sdstrim(s,"Aa. :");
 * printf("%s\n", s);
 *
 * Output will be just "HelloWorld".
 */
sds sdstrim(sds s, const char *cset) {
     
    char *start, *end, *sp, *ep;
    size_t len;

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

SDS 的 trim 等操作的缩容属于惰性删除, 而下面的 sdsRemoveFreeSpace 就是真正释放空间的操作。物理缩容说白了就是要将 SDS 的空间从 alloc 缩容到 len 长度,即 alloc - len = 0


/* Reallocate the sds string so that it has no free space at the end. The
 * contained string remains not altered, but next concatenation operations
 * will require a reallocation.
 *
 * 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 sdsRemoveFreeSpace(sds s) {
     
    void *sh, *newsh;
    // 获得 sds 类型,和结构体长度
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen, oldhdrlen = sdsHdrSize(oldtype);
    size_t len = sdslen(s);
    size_t avail = sdsavail(s);
    sh = (char*)s-oldhdrlen;

	// 如果 avail 已经为 0 了,没有可用空间了,则无需再缩了
    /* Return ASAP if there is no space left. */
    if (avail == 0) return s;

    /* Check what would be the minimum SDS header that is just good enough to
     * fit this string. */
    // 以 len 获得 type, 将空间从 alloc 降到 len 长度
    type = sdsReqType(len);
    hdrlen = sdsHdrSize(type);

    /* If the type is the same, or at least a large enough type is still
     * required, we just realloc(), letting the allocator to do the copy
     * only if really needed. Otherwise if the change is huge, we manually
     * reallocate the string to use the different header type. */
	// 如果类型不变,则重新分配空间
    if (oldtype==type || type > SDS_TYPE_8) {
     
        newsh = s_realloc(sh, oldhdrlen+len+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+oldhdrlen;
    } else {
     
    	// 类型发生变化则重新分配内存
        newsh = s_malloc(hdrlen+len+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, len);
    return s;
}
  • 基本逻辑跟扩容一样,没啥好特别说明的

参考资料


  • 《Redis 设计与实现》- @黄键宏
  • redis6.0源码动态字符串SDS - @地鼠工程师
  • redis6.0源码学习(二)sds - @程序员学编程
  • redis源码解析-字符串 - @静夜
  • 如果觉得对你有帮助,能否点个赞或关个注,以示鼓励笔者呢?!

你可能感兴趣的:(Redis,Redis,SDS,Redis,6.0,Redis,源码,源码分析)