SDS头文件及作用

  • sds.h: sds声明- sdsalloc.h: 为sds分配内存 源码文件sds.h中有这样一行代码
typedef  char *sds;

很清晰、明了,sds其实就是char*
最新的6.2分支的代码:

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

__attribute__ ((__packed__))的设置是告诉编译器取消字节对齐,则结构体的大小就是按照结构体成员实际大小相加得到的。
Redis是在3.2版本(包括3.2)之后把sdshdr改为现在这样的。

看到这五个结构体有点懵,我想程序都有个初始版本,万变不离其宗,我切换到2.2版本的分支上,看到了sdshdr最初的模样

struct  sdshdr {<!-- -->
int  len;
int  free;
char  buf[];
};

注意:这里的len是buf字符数组中,不包括最后的空字符的字符个数。sdshdr 是一个包含字符串数组和长度的结构体。

相比C字符串,SDS的优势

sdshdr和C字符串有什么优势和劣势呢?请继续往下看

获取字符串的时间复杂度

  • SDS字符串: O(1)- C字符串:O(n),需要遍历字符串,以\0结尾- 使用SDS可以确保获取字符串长度的操作不会成为Redis的性能瓶颈。

杜绝缓冲区溢出

  • C字符串不会记录自身的长度和空闲空间,容易造成缓冲区溢出,而SDS则不会,在拼接字符串之前,会通过free字段检测是否能满足数据的存放,如果不满足则会进行扩容。

减少修改字符串时带来的内存重分配次数

  • C字符串在对字符进行拼接或者缩短的情况下,都会对这个C字符串的内存进行重新分配。比如拼接字符串时,需要重新分配来扩展原有字符串数组的大小,避免溢出缓冲区;在对字符串进行缩短操作时,需要重新分配内存来释放不需要的那部分,避免内存泄漏。所以C语言中每次修改字符串都会造成内存重分配。
  • SDS字符串则有lenfree属性,可以实现两种内存分配和释放操作:内存预分配和内存惰性释放
      - 在对`SDS`进行扩展的时候,程序不仅会为`SDS`分配所必需的内存,还会为`SDS`分配额外的空闲内存。这样就减少连续增长字符串所需内存重新分配的次数。通过内存预分配,`SDS`将`N`次字符串增长操作所需内存分配的次数从必须`N次降低为最多`N`次。- 额外未分配的内存的大小的策略:在扩展sds空间之前,sds api会检查未使用的空间是否够用,如果够用则直接使用未使用的空间,无须执行内存重分配。如果不够用则重新分配内存:
      ![03eee2e2c83b2aabb255efde21e72a52.png](https://img-blog.csdnimg.cn/img_convert/03eee2e2c83b2aabb255efde21e72a52.png)- 内存释放策略:在`SDS`进行字符串缩短操作时,程序不会立马重新分配内存来缩短多出来的字节,而是使用属性`free`记录下来等待将来使用。通过惰性内存释放策略,`SDS`避免因缩短字符串操作而进行内存重新分配的次数,为将来有可能的增长操作带来了优化。- 可以通过sds api来释放未使用的空间,不用担心惰性空间释放策略会造成内存浪费

    二进制安全

    • 为了确保redis可以保存二进制数据(图片、视频等),SDS的API是二进制安全的。 程序不会对其中的数据做任何的限制,过滤,数据存进去是什么样子,读出来就是什么样子,这也是buf数组叫做字节数组而不是叫字符数组的原因。Redis不仅可以保存文本数据,还可以保存任意格式是二进制数。- C字符串中除了末尾的空字符,字符串其他位置不能包含空字符,所以C语言字符串只能保存文本数据,不能保存二进制数据。 总结:
      b96cd40f41f868808a756b452b89c90e.png

    6.2分支的SDS

    我们回到6.2的分支上,可以看到五个结构体可以归纳为一种包含Header数据包的结构体。
    3c4379a0864f369f8c0a397143803c26.png

    想要得到sdshdr的属性,需要知道header类型,flags字段存储了header类型,假如我们定义了sds* s,那么获取flags字段仅仅需要将s向前移动一个字节,即unsigned char flags = s[-1] , header的五类型如下:

    #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
    

    然后通过以下宏定义来对header进行操作:

    #define SDS_TYPE_MASK 7 // 类型掩码
    #define SDS_TYPE_BITS 3 
    #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); // 获取header头指针
    #define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) // 获取header头指针
    #define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS) // 获取sdshdr5的长度
    

    SDS的创建 & 扩容 & 销毁

    创建一个sds字符串函数:

    sds  sdsnew(const  char *init) {
        size_t  initlen = (init == NULL) ? 0 : strlen(init);
        return  sdsnewlen(init, initlen);
    }
    

    触发扩容操作:

    sds sdscatfmt(sds s, char const *fmt, ...) {
        ...
            switch(*f) {
            case '%':
                next = *(f+1);
                f++;
                switch(next) {
                case 's':
                case 'S':
                    str = va_arg(ap,char*);
                    l = (next == 's') ? strlen(str) : sdslen(str);
                    if (sdsavail(s) < l) {  //当剩余空间小于要加入数据的大小时需要扩容
                        s = sdsMakeRoomFor(s,l);  //扩容
                    }
                    memcpy(s+i,str,l);
                    sdsinclen(s,l);//设置  已使用字符长度 属性。
                    i += l;
                    break;
                ...
            }
            f++;
        }
        va_end(ap);
    
        /* Add null-term */
        s[i] = '\0';
        return s;
    }
    

    惰性删除操作, 设置 len字段为0,但是并没有释放内存。这是一个比较极端的例子,

    void sdsclear(sds s) {
        sdssetlen(s, 0);
        s[0] = '\0';
    }
    

    惰性删除操作,这个例子比较正常,如果新数据的长度小于等于当前数据的长度则不进行扩容,不扩容只需要修改len这个字段的值即可,不需要释放内存。反之则需要进行扩容。

    sds sdsgrowzero(sds s, size_t len) {
        size_t curlen = sdslen(s);
    
        if (len <= curlen) return s;  //新数据的长度小于等于数据的长度则不进行扩容。
        s = sdsMakeRoomFor(s,len-curlen); //扩容操作
        if (s == NULL) return NULL;
    
        /* Make sure added region doesn't contain garbage */
        memset(s+curlen,0,(len-curlen+1)); /* also set trailing \0 byte */
        sdssetlen(s, len); //设置已使用数据长度,len字段
        return s;
    }
    

    释放空闲空间

    sds sdsRemoveFreeSpace(sds s) {}
    

    销毁SDS

    void  sdsfree(sds  s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
    }
    

    --完--