redis源码分析(一)-sds实现

  redis支持多种数据类型,sds(simple dynamic string)是最基本的一种,redis中的字符串类型大多使用sds保存,它支持动态的扩展与压缩,并提供许多工具函数。这篇文章将分析sds在redis中是如何实现的。redis

1.    sds类型

  sds在redis中其实就是一个char*类型的别名,声明以下:app

typedef char *sds;

可是,以sds指向的字符串的存储格式具备必定的规则,即在字符串数据以前存储了相应的头部信息,这些头部信息包含了:1. alloc-分配的内存空间长度。2. len-有效字符串长度。3. flags-头部类型。函数

  redis中有sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64这5类头部类型,其声明以下:工具

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

其它几种类型相似,但len与alloc字段根据头部类型使用不一样的类型,sdshdr16使用uint16_t,sdshdr32使用uint32_t,sdshdr64使用uint64_t。另外,sdshdr5与其它头部类型不一样,没有len与alloc字段,并将字符串实际长度保存在flags字段的高5bits。ui

  sdshdr8因为alloc为uint8_t类型,所以能够表示的字符串最长为255字节;sdshdr16因为alloc为uint16_t类型,所以能够表示的字符串最长为65536字节;相似的,redis中选择最合适的头部去存储字符串,节约少量空间(我认为当字符串较短时,使用sdshdr8能够节约空间,但当字符串长度超过了uint8_t的表示范围,使用sdshdr16与sdshdr32,头部长度占所用空间的比例差异不大)。选择该使用何种头部的函数实现以下:this

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
}

2.    sds操做

2.1  sds类型与实际分配内存之间的转换

sds的类型指向有效的字符串起始位置,头部信息与有效字符串的存储空间是统一分配的,它们的内存空间的连续的,所以将sds向前移动头部长度,便可获得实际分配的内存起始地址。而sds头部长度由头部类型决定,其实现以下:spa

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);
        case SDS_TYPE_16:
            return sizeof(struct sdshdr16);
        case SDS_TYPE_32:
            return sizeof(struct sdshdr32);
        case SDS_TYPE_64:
            return sizeof(struct sdshdr64);
    }
    return 0;
}

sds的头部类型保存在头部的flags的低3bits,头部类型能够经过以下方式得到code

oldtype = s[-1] & SDS_TYPE_MASK; //s为sds类型

实际分配内存地址由以下方式得到blog

sh = (char*)s-sdsHdrSize(oldtype);

所此,sdsfree操做实现以下:内存

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

2.2  为c_str分配sds类型

  新建sds类型的函数声明为:

sds sdsnewlen(const void *init, size_t initlen)

它的大体步骤可描述以下:

  1. 根据c_str的长度选择合适的sds头部类型,这一步由sdsReqType()函数实现
  2. 分配足够的空间存储头部与有效字符串(sds字符串末尾须要一个字节存储’\0’),这一步由s_malloc()函数实现。
  3. 设置头部信息,将c_str内容copy到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 (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    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;
        }
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}

2.3  为sds增长存储空间

为已存在sds类型增长存储空间的操做函数声明为:

sds sdsMakeRoomFor(sds s, size_t addlen)

它的操做步骤大体以下:

  1. 查看sds中是否有足够的剩余空间容纳addlen长度的字符串,有则返回,无则继续其它操做。
  2. 计算须要从新分配的存储空间的长度,包括原sds长度与addlen,另外预备一部分的剩余空间。
  3. 根据新的长度,获得新的sds头部类型,若是新的头部类型与原类型相同,则使用s_realloc分配更多的空间;若是新的头部类型与原类型不相同,则使用s_alloc从新分配内存,并将原sds内容copy到新分配的空间。

函数具体实现以下:

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;

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)
        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) {
        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;
        sdssetlen(s, len);
    }
    sdssetalloc(s, newlen);
    return s;
}

2.4  释放sds未使用的存储空间

sds头部alloc记录了分配的内存空间,len记录了实际使用的内存空间,当须要释放未使用的内存空间时,根据len记录,可使用s_realloc压缩空间或者使用s_alloc从新分配更小的空间,释放旧的内存。相应函数声明为:

sds sdsRemoveFreeSpace(sds s) 

大体操做步骤以下:

  1. 根据len计算新的sds类型与所需内存空间。
  2. 若是新的类型与原sds类型相同,使用s_realloc压缩原内存空间。
  3. 若是新的类型与原sds类型不相同, 使用s_alloc从新分配空间,将原内容copy到新分配的sds中,并释放原内存。

sdsRemoveFreeSpace的实现与sds sdsMakeRoomFor大体相同,此处再也不列出。

 

另外sds还提供了不少工具函数,如cat操做,copy操做,ll2str(整数转字符串),vprintf操做(格式化操做)等。具体实现见源码文件sds.c。

相关文章
相关标签/搜索