1. 程式人生 > 實用技巧 >redis原始碼學習之sds

redis原始碼學習之sds

參考《Redis 設計與實現》 (基於redis3.0.0) 作者:黃健巨集
學習redis3.2.13

介紹
SDS結構
redis3.0.0中的結構
redis3.2.13中的結構
結構變化帶來的優勢與劣勢
SDS對比C字串的優勢
效能優勢
安全優勢
功能優勢
主要函式學習
主要函式速覽
sdsnewlen
sdsfree
sdstrim
sdscmp
sdsfromlonglong
sdsMakeRoomFor
sdsRemoveFreeSpace
總結toc

介紹

簡單動態字串SDS(simple dynamic strings) 是redis中的字串型別

SDS結構

redis3.0.0中的結構

3.0.0中的SDS比柔性陣列多了一個表示buf剩餘可用空間的成員free。此成員為SDS的空間管理提供了靈活性。

/*
 * 類型別名,用於指向 sdshdr 的 buf 屬性
 */
typedef char *sds;    //供外部使用的型別
/*
 * 儲存字串物件的結構
 */
struct sdshdr {
    // buf 中已佔用空間的長度,即字串長度,不包含結尾的空字元
    unsigned int len;
    // buf 中剩餘可用空間的長度
    unsigned int free;
    // 資料空間
    char buf[];
};

SDS結構演示例子:

redis3.2.13中的結構

3.2.13中的SDS可以根據不同的初始字串長度,選擇不同的sds頭部
頭部結構定義如下:

typedef char *sds;
//sdshdr5 中flags低3位用來存頭部類,高5位用來存字串長度
/* 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[];
};
  • len表示buf 中已佔用空間的長度,即字串的長度
  • alloc表示已申請的空間的長度除去頭部與結尾空字元佔用的部分,即字串長度與剩餘可用長度之和
  • flags用於區分頭部的型別,僅使用了低三位
    對於sdshdr5 ,註釋說不會被用到,我好奇的搜了一下後,發現情況不是那麼簡單。

結構變化帶來的優勢與劣勢

優勢:

  • 對於短小字串來說:取消記憶體對齊,並將頭部從固定的8位元組縮短到最低3位元組,節省了記憶體
  • 對於巨大字串來說:字串的長度描述從無符號的32位提高到了無符號64位
    • 可儲存的資料更大
    • 更能避免記錄長度的整形溢位,更安全

劣勢:

  • 當儲存的字串由巨大變為短小時,頭部並不會縮短,一定程度上浪費了記憶體

SDS也遵守C語音字串中以空字串結尾的規則,所以可以使用C語言字串函式庫中的部分函式。

SDS對比C字串的優勢

SDS相對於C字串有效能、安全性、功能方面的優勢

效能優勢

獲取字串長度的時間複雜度為O(1)
SDS中的len記錄了字串的長度,獲取長度時只需返回len即可,無需遍歷

//3.0.0中 直接偏移回頭部取長度成員
static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}

//3.2.13中 則需根據flags來偏移再取出長度
...
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3
...
#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;
}

獲取剩餘可用空間時間複雜度也是O(1)

//3.0.0中 也是直接偏移回頭部取剩餘可用空間
static inline size_t sdsavail(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->free;
}
//3.2.13中 同樣需根據flags來偏移,但需經過簡單計算來得到剩餘可用空間
...
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
...
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;
}

減少修改字串時重分配的次數

  1. 空間預分配
    當對SDS修改並且造成其需要擴容時,會為SDS分配額外空間並記錄其長度,以供後續使用:
    • 修改後的長度將小於1MB時,額外分配空間大小與len相同
    • 修改後的長度將大於等於1MB時,額外分配1MB空間
  2. 惰性空間釋放
    當對SDS進行縮減時,不會釋放被縮減字元佔用的記憶體,僅會計入剩餘可用空間,以備後續使用

可以看出,SDS通過額外的空間換取了一定的效能

安全優勢

杜絕緩衝區溢位
SDS內部記錄了剩餘可用空間的長度,對SDS進行追加時,會對剩餘可用空間進行判斷,剩餘可用空間不滿足需求時會對SDS擴容

功能優勢

二進位制安全(可儲存任意二進位制資料)

  • SDS以二進位制方式處理存在其中的資料,不對所存資料進行任何限制
  • SDS的長度由其len儲存,而不是空字元判斷

相容部分C字串函式
SDS仍然以空字串結尾,可重用string.h庫定義的函式

主要函式學習

主要函式速覽

原始碼為3.2.13

sdsnewlen

//根據string_size來選擇合適的頭部型別
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 (string_size < 1ll<<32)    //long long 1
        return SDS_TYPE_32;
    return SDS_TYPE_64;
}
//根據頭部型別得出頭部大小
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;
}

//根據initlen指定的長度,使用初始字串init來構建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);    //#define s_malloc zmalloc
    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;
}

sdsfree

//釋放SDS
void sdsfree(sds s) {
    if (s == NULL) return;
    //從頭部開始釋放
    s_free((char*)s-sdsHdrSize(s[-1]));    //#define s_free zfree
}

sdstrim

//從SDS兩端去掉包含在字串cset內的字元
// s = sdsnew("AA...AA.a.aa.aHelloWorld     :::");
// s = sdstrim(s,"Aa. :");=======>>>"Hello World"
sds sdstrim(sds s, const char *cset) {
    char *start, *end, *sp, *ep;
    size_t len;

    sp = start = s;
    ep = end = s+sdslen(s)-1;
    while(sp <= end && strchr(cset, *sp)) sp++;
    while(ep > sp && strchr(cset, *ep)) ep--;
    len = (sp > ep) ? 0 : ((ep-sp)+1);
    if (s != sp) memmove(s, sp, len);
    s[len] = '\0';
    sdssetlen(s,len);
    return s;
}

sdscmp

//對比兩個SDS
int sdscmp(const sds s1, const sds s2) {
    size_t l1, l2, minlen;
    int cmp;

    l1 = sdslen(s1);
    l2 = sdslen(s2);
    minlen = (l1 < l2) ? l1 : l2;
    cmp = memcmp(s1,s2,minlen);    //非strcmp,二進位制安全
    if (cmp == 0) return l1-l2;
    return cmp;
}

sdsfromlonglong

#define SDS_LLSTR_SIZE 21
//將longlong轉換為字串buf
int sdsll2str(char *s, long long value) {
    char *p, aux;
    unsigned long long v;
    size_t l;
    //迴圈內生成了反向字串  123 ===> "321"
    /* Generate the string representation, this method produces
     * an reversed string. */
    v = (value < 0) ? -value : value;
    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;
}
//通過longlong生成SDS
sds sdsfromlonglong(long long value) {
    char buf[SDS_LLSTR_SIZE];
    int len = sdsll2str(buf,value);

    return sdsnewlen(buf,len);
}

sdsMakeRoomFor

經sdsMakeRoomFor函式後的SDS,頭部只能變大,沒法變小

#define SDS_MAX_PREALLOC (1024*1024)
...
//為SDS擴容
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; //取SDS頭部現有型別
    int hdrlen;

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

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype); //sh指向頭部
    newlen = (len+addlen);
    //修改後長度小於1MB則雙倍擴容
    //否則,在按修改後長度+1MB擴容
    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); //#define s_realloc zrealloc
        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);  //#define s_malloc zmalloc
        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;
}

sdsRemoveFreeSpace

//去除SDS內的剩餘可用空間
sds sdsRemoveFreeSpace(sds s) {
    void *sh, *newsh;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    size_t len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);

    type = sdsReqType(len);
    hdrlen = sdsHdrSize(type);
    if (oldtype==type) {
        newsh = s_realloc(sh, hdrlen+len+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+hdrlen;
    } 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;
}

總結

  • 效能的提高可以通過合理的結構設計、額外的空間、減少系統呼叫與資料拷貝、合理地內聯程式碼 來實現
  • 為了二進位制安全最好不要以特定字元最為結束符,除非特定字元在使用場景出現概率為0 或 有額外結構彌補缺陷


來自為知筆記(Wiz)