Redis動態字符串SDS源碼學習

參考

redis數據結構:sds動態字符串git

redis源碼解讀(一):基礎數據結構之SDSgithub

1. 用Simple Dynamic String 取代 C 默認的 char* 類型

Redis沒有直接使用c語言的字符串,而是本身定義了一個字符串數據結構,SDS做爲默認的字符串,咱們設置的全部鍵值基本都是SDSredis

C語言字符串特色:數組

  • 每次計算字符串長度strlen(s)的時間複雜度爲O(n)
  • 使用\0終止符斷定一個字符串的結尾,這種規則使得C語言的字符串是二進制不安全的
  • 對字符串進行N次追加,一定須要對字符串進行N次內存重分配

那麼從性能考慮,上面三個問題能夠這麼解決:安全

  • 如何實現O(1)時間複雜度的長度查詢:
    • 使得字符串數據結構中含有自身長度屬性 => 自定義字符串數據結構
  • 如何實現二進制安全:
    • 由於實現了length屬性,再也不須要以某種特殊格式(\0)解析數據,因此二進制安全了
  • 如何減小內存重分配的次數:
    • 按照必定的機制來決定拓展內存的大小,而後再執行追加操做,拓展後多餘的空間不釋放,方便下次再追加數組,這樣的代價就是浪費了一點內存,可是實現了用空間換時間的效果

2. Redis SDS的數據結構

github 源碼src/sds.h,結構體聲明代碼以下:bash

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

// 經過buf獲取頭指針
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
複製代碼

上方雖然聲明瞭sdshdr5sdshdr8sdshdr16sdshdr32sdshdr64五種類型,但均可以歸納爲:數據結構

  • len記錄當前字節數組的長度不包括\0
  • alloc記錄了當前字節數組總共分配的內存大小,不包括\0
  • flags記錄了當前字節數組的SDS_TYPE
  • buf保存了字符串真正的值以及末尾的一個\0

看看一個sdshdr8的實例, 整個SDS的內存是連續的,統一開闢的,經過這樣的方式就能經過buf頭指針進行尋址,拿到整個struct的指針curl

sdshdr8實例

2.1 attribute ((packed))的做用:勤儉持家省內存

編譯器內存對齊的優化策略:struct的分配的內存是內部最大元素的整數倍性能

其中__attribute__ ((__packed__))的做用爲:告訴編譯器不要對這個結構體進行優化對齊,讓結構體內部的字段與字段之間緊挨在一塊兒優化

printf("%ld\n", sizeof(struct sdshdr8));  // 3
printf("%ld\n", sizeof(struct sdshdr16)); // 5
printf("%ld\n", sizeof(struct sdshdr32)); // 9
printf("%ld\n", sizeof(struct sdshdr64)); // 17
複製代碼

sdshdr32爲例,其內部最大元素爲4(uint32_t佔4字節),不進行內存對齊,節省了4*3 - 9 = 3字節,同理,sdshdr64節省了8*3 - 17 = 7字節。

2.2 爲何會有sdshdr五、sdshdr8等區分:勤儉持家省內存

在絕大多數場景下,沒有開發者會給key取一個特別長的名字,將這些key變成sds字符串,就要在sdshdr.len中存放這些字符串的長度,如何選擇len的類型?

  • uint8: 確定會有字符串的長度超過2^8 - 1
  • uint16: 確定會有字符串的長度超過2^16 - 1
  • uint32: 確定會有字符串的長度超過2^32 - 1
  • uint64: 99%的狀況下字符串長度都是很是簡短的,用8個字節來存長度,極端浪費

所以在建立時,先計算出字符串的長度,根據長度,把sdshdr分爲幾種類型,達到節省內存的效果,能夠看到另外一個小細節:sdshdr5直接省掉了len字段, 用高5位存放長度,低3位存放類型,因此後面的結構有/* 3 lsb of type, 5 unused bits */這樣的註釋

sds類型內存圖

3. 建立SDS

調用:

mysds = sdsnewlan("abc", 3);
複製代碼

解析見註釋

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    // 根據內容長度`initlen`,肯定`SDS_TYPE
    char type = sdsReqType(initlen);
    // 空字符串使用SDS_TYPE_8類型,由於空字符串一般用於追加操做,SDS_TYPE_5不適合
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    // 獲取結構體大小
    int hdrlen = sdsHdrSize(type);
    // flags pointer
    unsigned char *fp;
    // 分配內存:結構體大小+字符串大小+1(`\0`)
    sh = s_malloc(hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        // 空字符串初始化內存爲0
        memset(sh, 0, hdrlen+initlen+1);
    s = (char*)sh+hdrlen;
    // 得到flags指針
    fp = ((unsigned char*)s)-1;
    switch(type) {
        case SDS_TYPE_5: {
            // SDS_TYPE_5的flags字段前5位保存長度後3位保存type
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);       // 得到sdshdr的指針
            sh->len = initlen;      // 設置len
            sh->alloc = initlen;    // 設置alloc
            *fp = type;             // 設置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';              // 字符數組最後一位設置爲\0
    return s;
}
複製代碼

4. 拼接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);       // 更新len屬性
    s[curlen+len] = '\0';           // 末尾追加一個\0
    return s;
}
複製代碼

重點在於sdsMakeRoomFor, 經過策略,減小拼接操做的內存分配次數

sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    // 獲取可用長度,即sh->alloc - sh->len;
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;

    // 剩餘空間足夠,無需擴容,返回
    if (avail >= addlen) return s;
    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    // 分配策略:小於1mb,內存翻倍,不然多分配1m
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;

    // 對於SDS_TYPE_5有一句註釋:sdshdr5 is never used
    type = sdsReqType(newlen);
    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 {
        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總結:

  • 經過sds->len將獲取字符串長度的時間複雜度下降到了O(1),進而使得字符串不受限於C字符串的\0終止符,實現二進制安全
  • 經過內存預分配策略(小於1mb翻倍,不然增長1mb)減小拼接操做的內存重分配次數:空間換時間
  • 總會在sds->buf的末尾追加一個\0,在部分場景下和C語言字符串保持一樣的行爲
  • 對於內部來講:整個SDS的內存是連續的,能夠經過尋址方式定位到任意一個值
相關文章
相關標籤/搜索