redis sds

在任何语言和系统中,字符串都几乎是使用频率最高的数据结构,而标准的c字符串有一些问题,主要包括:

  1. 字符串必须以’\0’结尾,导致图片等二进制不安全的数据无法以字符串形式存储;
  2. 字符串统计长度的复杂度为O(n),调用strlen时会对整个字符串从头到尾遍历,如果不小心在外层加了一层循环,会导致复杂度变更高,这点上没有java或者python方便;
  3. 容易发生溢出的问题,字符串是以’\0’为结尾的,一旦操作不慎,会在拼接字符串时,溢出到别的内存区域;
  4. 拼接字符串比较费时,3中说的一般是字符数组,字符串变量本身不能直接拼接,每次执行cat时,需要计算出两个字符串的长度,然后开辟一个全新的大小合适的空间,在那里写入新的字符串,而malloc这个系统调用实在是太慢了,很多高级语言为了避免这个方法调用,都使用了缓存,而标准的c似乎没有,至少字符串操作时不行;

以上四点是目前我能想到的c字符串的问题,或许还有其他,慢慢补充。

sds基本结构

sds是simple dynamic string的缩写;鉴于字符串在sds中基本就是最重要的,基本上哪里都得用到它,redis对字符串做了很多改良性的工作。
粗略的看,sds的基本结构是这样的:

struct sdshdr {
    int len; // buf中已使用字节的数量
    int alloc; // buf中总共分配的大小(除了末尾的'\0')
    char buf[]; // 字节数组,保存字符串;
}

以上是sds的基本结构,类似于面向对象语言,sds将字符串的长度作为属性值保存在了一个结构体中,同时开了一个比字符串本身要长的空间做缓存,用alloc来标记这个分配的空间的大小(虽然sds有len属性标记字符串长度,但是依然会在每个字符串最后加一个’\0’,必要情况下,可以直接使用c的接口;这个末尾的空字符没有记在alloc中,因此实际的buff部分长度是alloc+1,这点要注意)。
以上只是对sds的简化,实际上redis源码中,sds有五种数据结构(参见sds.h文件),如下:

typedef char *sds; // 好处很明显,可以直接与c字符串既有接口通用;

/* 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根据初始创建对象时字符串的长度,创建了五种结构体,其中第一种长度最短,按照作者的意思,这个是没有使用的,而其它四种结构完全一样,区别只是长度区间不同而已。
这五种类型中都有一个flags字段,是一个单字节字段,区分sds类型的参数就在这里边,最低三位表示sds类型,而类型的值包括:

#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 // 因为是最低三位存储类型,111;
#define SDS_TYPE_BITS 3 // 为了sds5计算长度时右移使用;

这里还有一点要注意,定义结构体时,使用了__attribute__((packed)),正常的c语言中,分配内存时会执行字节对齐,这就可能导致字段之间会不连续;而使用这个字段,会强制各个字段连续存储而不对齐,比如在所有结构体中,buf与flags永远是相邻两个字节,而从len到alloc到flags也是相邻的,redis在计算对象的实际起始地址时,使用了这个特征,这个要注意。

sdshdr5

这种结构下的字符串长度最短,而且没有alloc和len字段,即没有冗余空间。在其它类型中,flags的高5位是不使用的,但这个会使用高五位记录buf字段的大小;

其它四种

剩余四种整体结构一致,区别只是len和alloc的大小区间不一样,如sds8只用一个字节,而16使用了2个字节,其它类似;

sds接口

获取sds字符串长度:

#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) // 获取其它四种类型实例的起始指针,##T是个特殊的标记符,编译时会将T替换成相关的类型数字值;
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS) // 获取sds5类型的buf长度

static inline size_t sdslen(const sds s) {
    unsigned char flags = s[-1]; // c中支持负索引,此时代表指针减1,而不是常规的指针加1,指针减1正好偏移到flags字段;
    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;
}

由于两种不同结构体的存在,在获取长度时,就需要判断类型;
首先要注意的是,虽然结构体叫做sdshrd*,但是在对外的接口中,实际的参数类型都是sds,而它本质上是一个指向buf数组的char*指针,我们使用sds时必须使用这个类型而不能是sdshdr这个结构体家族,特别是释放内存的时候,不然会出错的;
其次,flags字段和buf字段是紧挨着的,而且都是单字节,因此根据buf的位置可以立马就能找到flags字段;
第三,具体的结构体类型存储在flags中的低三位,因此通过flags做位操可以获取到类型,即上面的&操作;
第四,其它四种都是要先通过SDS_HDR获取到类型对象的起始指针,然后直接读取len字段;而sds5就不同了,该类型直接使用flags的高5位存储字符串,所以它需要做的是对flags右移取值;

获取sds中未使用部分的容量大小:

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;
        }
        ... // 其余同sds8。
    }
    return 0;
}

基本同sdslen,唯一的区别是sds5没有未使用部分,它的已使用大小永远都是buf的字符串大小,因此永远返回0;

给sds设置新的字符串长度:

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;
        ... //同sds8
    }
}

获取sds头部大小

static inline int sdsHdrSize(char type) {
    switch(type&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            return sizeof(struct sdshdr5);
        case SDS_TYPE_8:
            return sizeof(struct sdshdr8);
        ... // 其它雷同8
    }
    return 0;
}

这个方法用来获取sds头部(不包括buf字段,在c中,像buf这样不声明大小的数组,被认为是一个占位符,本身是不计入头部大小的,需要有其它手段协助才能知道)大小;

根据需要的buff大小,返回需要的sds类型:

static 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)
        return SDS_TYPE_32;
    return SDS_TYPE_64;
#else
    return SDS_TYPE_32;
#endif
}

以上这些接口都是static的,基本是内部的sds中其它接口被调用时使用。

创建一个新的sds对象

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);
    /* 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);
    unsigned char *fp; /* flags pointer. */

    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; // 指向flags字段
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s); // 宏定义展开,操作后,sh将被转为相应sds类型的对象,并指向该对象的起始位置;
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        ... // 其它三种同sds8
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

这个接口用来创建一个新的sds对象。
首先,根据传入的size选择使用哪种结构体,sds5虽然不被推荐使用,不过这里并没有完全废弃,当传入的initlen足够小但还不是0时,依然使用了sds5;这里还需要注意,在分配大小时,尾部多分配了一个用来存储’\0’的字节;
第二步,设置flags的值,只有sds5需要同时设置长度和类型,其它都是单独分开设置的;
第三步,如果init不是空指针,就把该字段的值复制到新开的buf区域中,这里不用担心有溢出发生,因为initlen已经保证了复制操作不可能溢出;另外就是末尾设置了’\0’;
比较奇怪的地方是不知都为什么init是一个void而不是char,好像是为了多态?因为在后面dsdup中页调用了这个函数,但传入的是sds而不是char*;

根据给定的字符串创建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);
}

本质上就是调用sdsnewlen函数,不过它只接收字符串作为参数。

复制sds对象

sds sdsdup(const sds s) {
    return sdsnewlen(s, sdslen(s));
}

通过传入的sds对象,构造一个各参数内容一样的对象出来;

释放sds对象

void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1])); // 计算出真正的起始位置;
}

这里是sds释放时应该被调用的方法,因为sds只是指向buf的指针,整个对象的起始位置不在这里,所以千万不能调用free直接操作,搞不好会爆炸的;

扩展sds buf大小

sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    size_t avail = sdsavail(s); // 计算当前buf中还有多少自由空间,如果足够的话就不重新分配;
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK; // 需要保存当前sds类型,因为扩容可能会引起类型变化;
    int hdrlen;

    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC) //#define SDS_MAX_PREALLOC (1024*1024)。即1M.
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;

    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;

    hdrlen = sdsHdrSize(type);
    if (oldtype==type) { // 扩容前后类型一样,直接开辟一个新空间把所有值复制过去就行,除了alloc字段;
        newsh = s_realloc(sh, hdrlen+newlen+1); // realloc会自动将sh中的每个字节按序copy到新开辟的空间中,并释放sh指向的空间;
        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;
        sdssetlen(s, len);
    }
    sdssetalloc(s, newlen);
    return s;
}

当要给一个sds进行扩容时使用,这里的addlen不是值扩容后buf分配的大小,而是指冗余的大小;
首先计算当前剩余的空间,如果比addlen大,就不分配直接返回;
然后根据上一步计算出的新的长度确定需要给buf分配的真实大小,sds在分配时不是说要有addlen的容量就一定给addlen的容量,它只是保证自由空间大小不会比整个值小而已,上一步就是这样的,而这一步中,sds会在需求的空间大小上,再进一步扩大整个空间,因为这种需求往往来自于append,而append如果出现连续操作的情况,则可能会导致不停的系统调用,因此sds在分配时实际上给的空间要比需要的大,根据新大小的需求不同分配的大小不同:

  1. 分配后的新的空间不超过1M,就给它翻倍的空间;
  2. 分配后的空间超过了1M,就只给它多1M的额外空间,不然内存怕是扛不住的;

第三步,根据重新计算后需要的大小来判断新分配的sds头类型,因为这种扩容操作极有可能造成类型升级,所以需要看一下是否整个头结构都要变化,因为sds5没有冗余,所以至少要使用sds8;
第四步,这里开始将原始值复制到新的目标空间,如果扩容前后类型一样,那么只要将原来的各个字节从头到尾copy到新空间即可;如果类型不同,除了copy内容外,还要设置flags字段和len字段;
最后一步,扩容后,不管哪种情况,alloc字段都需要重新设置;

清除sds结尾的冗余空间

sds sdsRemoveFreeSpace(sds s) {
    void *sh, *newsh;
    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;

    /* 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. */
    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) { // 为什么type > sds8 时也可以呢?
        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;
}

这个方法没有什么特殊的地方,唯一没有想明白的是为什么缩容前后类型不同但都是sds8以上的内容时,可以直接复制呢?

获取整个sds对象的大小

size_t sdsAllocSize(sds s) {
    size_t alloc = sdsalloc(s);
    return sdsHdrSize(s[-1])+alloc+1;
}

这个值包含了头部大小、buf有效字符部分、冗余部分、最后的’\0’部分。

将sds len增加到指定size

void sdsIncrLen(sds s, ssize_t incr) {
    unsigned char flags = s[-1];
    size_t len;
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5: {
            unsigned char *fp = ((unsigned char*)s)-1;
            unsigned char oldlen = SDS_TYPE_5_LEN(flags);
            assert((incr > 0 && oldlen+incr < 32) || (incr < 0 && oldlen >= (unsigned int)(-incr)));
            *fp = SDS_TYPE_5 | ((oldlen+incr) << SDS_TYPE_BITS);
            len = oldlen+incr;
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            assert((incr >= 0 && sh->alloc-sh->len >= incr) || (incr < 0 && sh->len >= (unsigned int)(-incr)));
            len = (sh->len += incr);
            break;
        }
        ... // 其它类似于sds8
        default: len = 0; /* Just to avoid compilation warnings. */
    }
    s[len] = '\0';
}

这个方法应该是不能单独被使用的,按照作者的意思,当调用上面的sdsMakeRoomFor后,可能会使用这个方法,作者举例如下:

* oldlen = sdslen(s);
 * s = sdsMakeRoomFor(s, BUFFER_SIZE);
 * nread = read(fd, s+oldlen, BUFFER_SIZE);
 * ... check for nread <= 0 and handle it ...
 * sdsIncrLen(s, nread);

可以将sds作为输出的直接位置而不用使用中间缓冲区,此时在将内容写入完毕后,可以调用sdsIncrLen函数,将len设置成指定长度。
它的逻辑也比较简单,有两点需要注意:
1)incr参数可正可负,负表示要缩短len的大小,但正数时不能溢出,因为不会扩容,而负数时不能将长度缩短到小于0;代码中通过assert进行判断;
2 case最后使用了default,作者的意思是要防止编译警告,不过在之前的其它地方却都没有这么干过,不知道这里为什么就需要单独警告,改天编译起来看看效果;

填充sds到指定长度

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);
    return s;
}

这个函数没什么特色,就是将sds用0填充到指定长度,要是原始len本来就比目标len长,无操作;

向sds中追加二进制数据

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

没什么特色,只是由于是二进制数据,结尾不知道在哪里,所以需要专门传入长度参数。

向sds中追加字符串数据

sds sdscat(sds s, const char *t) {
    return sdscatlen(s, t, strlen(t));
}

更没什么特色,直接就是调用上面的方法,只是这次有\0结尾知道长度而已。

用char *更新sds数据

sds sdscpylen(sds s, const char *t, size_t len) {
    if (sdsalloc(s) < len) {
        s = sdsMakeRoomFor(s,len-sdslen(s));
        if (s == NULL) return NULL;
    }
    memcpy(s, t, len);
    s[len] = '\0';
    sdssetlen(s, len);
    return s;
}

sds sdscpy(sds s, const char *t) {
    return sdscpylen(s, t, strlen(t));
}

没啥,只是上面的sdscpylen函数比较奇怪,看作者的注释和代码中做的,是用*t的内容完全替换既有的sds中的内容,但是当空间不足时,作者居然使用了sdsMakeRoomFor函数而不是完全新建一个新的,不太能理解;

long long int转换成字符串

#define SDS_LLSTR_SIZE 21
int sdsll2str(char *s, long long value) {
    char *p, aux;
    unsigned long long v;
    size_t l;

    /* Generate the string representation, this method produces
     * a reversed string. */
    v = (value < 0) ? -value : value; // 因为v是unsigned类型,所以不会溢出;
    p = s;
    do {
        *p++ = '0'+(v%10);
        v /= 10;
    } while(v);
    if (value < 0) *p++ = '-';

    /* Compute length and add null term. */
    l = p-s;
    *p = '\0';

    /* Reverse the string. */
    p--;
    while(s < p) {
        aux = *s;
        *s = *p;
        *p = aux;
        s++;
        p--;
    }
    return l;
}

这里的char *s参数是指向一片有效的字符串空间,且长度足够长,至少不短于SDS_LLSTR_SIZE;另外为了防止在将value转成正数时发生溢出,v使用unsigned类型;返回的类型是转换后字符串的长度;

unsigned long long转换字符串

int sdsull2str(char *s, unsigned long long v)

这个类似上面,它连溢出都不需要考虑;

用long long 创建sds对象

sds sdsfromlonglong(long long value) {
    char buf[SDS_LLSTR_SIZE];
    int len = sdsll2str(buf,value);

    return sdsnewlen(buf,len);
}

直接调用上面的sdsll2str方法。
还有其它的一些接口,基本都类似与上面,不再列出。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值