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。

相關文章
相關標籤/搜索