美文网首页Redis
Redis5.x底层数据结构之——字符串

Redis5.x底层数据结构之——字符串

作者: Jerry_1116 | 来源:发表于2019-01-23 00:12 被阅读0次

    1 简单动态字符串

    Redis是用C语言写的,但是Redis的字符串不是 C 语言中的字符串(即以空字符’\0’结尾的字符数组)。Redis自己定义了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS作为Redis的默认字符串表示。
    SDS的定义在sds.h中:

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

    SDS由4部分组成:
    len:SDS字符串已经使用的空间(不包含C中字符串的结束符'\0'的长度1)。
    alloc:申请的空间大小,减去len就是未使用的空间,初始时和len相等。
    flags:使用低三位表示类型,细分SDS的分类。方便根据字符串的长度不同选择不用的SDS结构体,节省一部分空间。
    buf:使用C的不定长字符串。

    从SDS的定义上看,5种定义对应了最大长度不同的字符串。定义这5种不同的类型是为了尽量减少sdshdr占用的空间。
    为了区分sdshdr属于哪一种类型,在每一种定义中都加上了一个8bit的flags字段,用其中的低3位标识sdshdr的类型。
    在C/C++中,建立一个结构体时,会进行字节对齐操作,使得结构体的大小比其变量占用的字节要多一些。为了能从buf直接找到到flags,定义时在结构体声明中加上__attribute__((__packed__)), 强制不要按字节对齐(表示取消字节对齐,按照紧凑排列的方式),这样不管是哪种类型的hdr,都可以用buf[-1]找到对应的flags。

    2 SDS与C字符串的区别

    C语言使用长度为N+1的字符串数组来表示长度为N的字符串;字符串数组的最后一个元素一定是'\0'。C语言的这种表示字符串的方式,并不能满足Redis对字符串在安全性、功能性以及效率性的要求。
    使用SDS除了用C语言中的字符串数组buf[]表示了字符串的内容,还记录了Redis为该字符串分配的buf空间的总长度alloc、buf[]中已经使用的长度len。Redis使用SDS有以下几个好处。

    2.1 常数复杂度获取字符串长度

    由于存在len属性,获取SDS字符串的长度只需要读取len属性,时间复杂度为O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为O(n)。通过strlen key命令获取key的字符串长度的时间复杂度为O(1),可以反复执行而不会出现性能瓶颈。

    2.2 杜绝缓冲区溢出

    在 C 语言中使用strcat函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的len属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。

    2.3 减少修改字符串的内存重新分配次数

    C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。
      而对于SDS,由于len属性和alloc属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略:

    1. 空间预分配
      对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。
      额外分配空间策略:
    • 1.1. 如果SDS修改之后,SDS的长度(len属性的值)将小于1MB(1024*1024 Byte),那么程序将分配和len属性相同的大小的未使用空间,分配之后,alloc=len2。
      示例:
      SDS当前的长度len为6,alloc为12。此时执行append方法,在后面追加
      hello redis!*字符串,修改之后的长度len将变成18。修改之后,SDS的len为18,同时分配18Byte的未使用空间,alloc为36。最终,buf[]的实际长度将变成alloc+1Byte=36Byte+1Byte(1Byte用于保存空字符'\0')=37Byte。

    • 1.2 如果SDS进行修改之后,SDS的长度将大于等于1MB(1024*1024 Byte),那么程序会分配1MB的未使用空间。
      示例:
      SDS当前长度为0,len=0,alloc=0。此时执行set方法,字符串长度为2MB。那么执行之后,len=2*1024*1024,alloc=len+1024*1024=3*1024*1024,buf[]的实际长度为:2MB+1MB+1Byte。
      0sds.h中:

    #define SDS_MAX_PREALLOC (1024*1024)
    

    sds.c中

        newlen = (len+addlen);
        if (newlen < SDS_MAX_PREALLOC)
            newlen *= 2;
        else
            newlen += SDS_MAX_PREALLOC;
    
    1. 惰性空间释放
      对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用alloc属性将所有字节的数量记录下来,等待后续使用(当然SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间)。

    2.4 二进制安全

    因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片、音频、视频、压缩文件等),内容可能包括空字符串,因此C字符串无法正确存取;而所有 SDS 的API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束,因此SDS是二进制安全的。Redis的buf[]是字节数组,因为buf[]不是用于保存字符,而是保存一系列二进制数据。Redis使用二进制安全的SDS,可以保存任意格式的二进制数据。

    2.5 兼容部分 C 字符串函数

    虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库<string.h> 中的一部分函数,避免了不必要的代码重复。

    综上,用下面表格总结C字符串和SDS之间的区别。

    C字符串 SDS
    获取字符串长度的复杂度为O(N) 获取字符串长度的复杂度为O(1)
    API是不安全的,可能会造成缓冲区溢出 API是安全的,不会造成缓冲区溢出
    修改字符串长度N次需要执行N次的内存重分配 修改字符串长度N次最多需要执行N次的内存重分配
    空字符'\0'作为文本数据的结束,只能保存文本数据 二进制安全,可以保存文本数据和所有格式的二进制数据
    可以使用所有的C语言库中的函数,如<string.h>/strcasecmp 可以使用部分C语言库中的函数,如<string.h>/strcasecmp

    3 SDS常用API

    SDS常用API和常量定义。此处不详细解释,基本都可以根据函数名知道函数功能。
    详细参考Redis的sds.h源码和sds.h的实现sds.c源码。

    #ifndef __SDS_H
    #define __SDS_H
    
    #define SDS_MAX_PREALLOC (1024*1024)
    const char *SDS_NOINIT;
    
    #include <sys/types.h>
    #include <stdarg.h>
    #include <stdint.h>
    
    typedef 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[];
    };
    
    #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
    #define SDS_TYPE_BITS 3
    #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)
    
    static 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;
    }
    
    static inline size_t sdsavail(const sds s) {
        unsigned char flags = s[-1];
        switch(flags&SDS_TYPE_MASK) {
            case SDS_TYPE_5: {
                return 0;
            }
            case SDS_TYPE_8: {
                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;
    }
    
    static 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;
        }
    }
    
    static inline void sdsinclen(sds s, size_t inc) {
        unsigned char flags = s[-1];
        switch(flags&SDS_TYPE_MASK) {
            case SDS_TYPE_5:
                {
                    unsigned char *fp = ((unsigned char*)s)-1;
                    unsigned char newlen = SDS_TYPE_5_LEN(flags)+inc;
                    *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
                }
                break;
            case SDS_TYPE_8:
                SDS_HDR(8,s)->len += inc;
                break;
            case SDS_TYPE_16:
                SDS_HDR(16,s)->len += inc;
                break;
            case SDS_TYPE_32:
                SDS_HDR(32,s)->len += inc;
                break;
            case SDS_TYPE_64:
                SDS_HDR(64,s)->len += inc;
                break;
        }
    }
    
    /* 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;
    }
    
    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;
        }
    }
    
    sds sdsnewlen(const void *init, size_t initlen);
    sds sdsnew(const char *init);
    sds sdsempty(void);
    sds sdsdup(const sds s);
    void sdsfree(sds s);
    sds sdsgrowzero(sds s, size_t len);
    sds sdscatlen(sds s, const void *t, size_t len);
    sds sdscat(sds s, const char *t);
    sds sdscatsds(sds s, const sds t);
    sds sdscpylen(sds s, const char *t, size_t len);
    sds sdscpy(sds s, const char *t);
    
    sds sdscatvprintf(sds s, const char *fmt, va_list ap);
    #ifdef __GNUC__
    sds sdscatprintf(sds s, const char *fmt, ...)
        __attribute__((format(printf, 2, 3)));
    #else
    sds sdscatprintf(sds s, const char *fmt, ...);
    #endif
    
    sds sdscatfmt(sds s, char const *fmt, ...);
    sds sdstrim(sds s, const char *cset);
    void sdsrange(sds s, ssize_t start, ssize_t end);
    void sdsupdatelen(sds s);
    void sdsclear(sds s);
    int sdscmp(const sds s1, const sds s2);
    sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count);
    void sdsfreesplitres(sds *tokens, int count);
    void sdstolower(sds s);
    void sdstoupper(sds s);
    sds sdsfromlonglong(long long value);
    sds sdscatrepr(sds s, const char *p, size_t len);
    sds *sdssplitargs(const char *line, int *argc);
    sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);
    sds sdsjoin(char **argv, int argc, char *sep);
    sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen);
    
    /* Low level functions exposed to the user API */
    sds sdsMakeRoomFor(sds s, size_t addlen);
    void sdsIncrLen(sds s, ssize_t incr);
    sds sdsRemoveFreeSpace(sds s);
    size_t sdsAllocSize(sds s);
    void *sdsAllocPtr(sds s);
    
    /* Export the allocator used by SDS to the program using SDS.
     * Sometimes the program SDS is linked to, may use a different set of
     * allocators, but may want to allocate or free things that SDS will
     * respectively free or allocate. */
    void *sds_malloc(size_t size);
    void *sds_realloc(void *ptr, size_t size);
    void sds_free(void *ptr);
    
    #ifdef REDIS_TEST
    int sdsTest(int argc, char *argv[]);
    #endif
    
    #endif
    

    相关文章

      网友评论

        本文标题:Redis5.x底层数据结构之——字符串

        本文链接:https://www.haomeiwen.com/subject/gpcqjqtx.html