Redis 2.8.9源碼 - Redis中的字符串實現 sds

本文爲做者原創,轉載請註明出處:http://my.oschina.net/fuckphp/blog/269167php


在C中子字符串的實現都是用 char *來實現的,用起來很不方便,並且容易出現內存泄露,而且效率不高,在Redis內部,字符串採用了 sds 的方式進行了封裝,似的字符串在Redis中能夠方便、高效的使用,Redis字符串的實現如要依賴一下兩個數據類型和結構(如下代碼能夠在 src/sds.h中找到):redis

typedef char *sds;

sds 存放了字符串的具體值curl

struct sdshdr {
    int len;   //字符串對象已經使用的內存數量
    int free;  //字符串對象剩餘的內存數量
    char buf[]; //字符串對象的具體值(其實就是sds字符串)
};

sdshdr 實現了字符串對象函數

這樣設計的好處有不少,好比使得Redis在獲取字符串長度的時候能夠達到o(1)的複雜度,在進行追加等字符串操做的時候,能夠減小內存分配(提升性能),sdshdr的結構使得根據sds字符串獲取對應的sds對象的時候能夠很是方便的獲取。性能

  1. 建立字符串 init 爲須要初始化的字符串值。initlen表示爲初始化字符串的長度,該函數建立一個sds字符串對象並返回sds字符串(如下代碼能夠在 src/sds.c中找到):url

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh; //建立一個空的字符串對象

    //若是init爲空的時候須要對分配的內存進行初始化
    if (init) {
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    } else {
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
    }   
    if (sh == NULL) return NULL;
    //設置字符串對象的已佔用長度
    sh->len = initlen;
    sh->free = 0;
    //若是init不爲空將其複製到字符串對象中
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    //在結尾加入終止符(c語言字符串以\0爲結尾)
    sh->buf[initlen] = '\0';
    //返回字符串對象中的sds值
    return (char*)sh->buf;
}

例如你建立了一個sds字符串 爲 hello 那麼你的代碼應該以下:
spa

sds str = sdsnewlen("hello", 5);

這時候,Reids會建立一個sdshdr對象,長度爲:.net

sizeof(struct sdshdr) + 5 + 1

Redis在釋放字符串也會分方便,由於是對整個結構進行的分配因此只須要對sds字符串的對象進行釋放就能夠將字符串值和字符串對象的內存都釋放掉,以下:設計

void sdsfree(sds s) {
    if (s == NULL) return;
    zfree(s-sizeof(struct sdshdr));
}

釋放上例中的sds字符串只須要簡單的調用:指針

sdsfree(str);

  2.   根據sds字符串獲取對應的字符串對象 

如1中你知道了如何建立一個字符串對象並返回它的sds字符串,根據sdshdr的存儲結構,你能夠方便的經過返回的sds字符串獲得字符串對象,以下代碼:(下面代碼中s表示sds字符串,定義爲 sds s;)

struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));

由1中建立的hello的字符串對象在內存中的分配大概以下圖:

上例中的 s 爲 sdshdr結構中的buf元素,上例代碼中的 s - sizeof(struct sdshdr) 會將指向buff 的指針,移動到len上,這樣經過一個簡單的運算就能夠獲取到sds字符串的對象,並對其進行字符串操做(不知道爲啥redis寧肯每次手動寫,也沒有對此進行一個宏定義的封裝)。

  3.  計算字符串長度

計算長度的方式就很是簡單了只須要根據sds字符串獲取到sds對象,而後獲取其len屬性便可,具備o(1)的效率,而不須要去遍歷字符列表,以下獲取方法(如下代碼能夠在 src/sds.h中找到)

static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}


  4.  追加字符串

Redis的追加字符串因爲其設計方式能夠很是高效,進行追加,直接看代碼(如下代碼能夠在 src/sds.c中找到)

sds sdscatlen(sds s, const void *t, size_t len) {
    struct sdshdr *sh;      //定義一個字符串對象
    size_t curlen = sdslen(s);   //獲取當前sds字符串的長度 能夠參考第3條

    s = sdsMakeRoomFor(s,len);   //對sds字符串擴展,申請len長度的內存(會根據free決定是否申請,見下文)
    if (s == NULL) return NULL;
    sh = (void*) (s-(sizeof(struct sdshdr)));   //根據新申請空間後的sds字符串獲取對應的對象
    memcpy(s+curlen, t, len);    //將新的字符串追加到結尾
    sh->len = curlen+len;     //更新已佔用空間
    sh->free = sh->free-len;  //更新剩餘空間
    s[curlen+len] = '\0';     //設置字符串結尾
    return s;     //返回修改後的sds字符串
}

   對sds字符串內存進行擴展的函數以下:(如下代碼能夠在 src/sds.c中找到):

sds sdsMakeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;   //初始化兩個字符串對象
    size_t free = sdsavail(s);   //字符串剩餘內存,定義在src/sds.h中,獲取方法與 sdslen()相同
    size_t len, newlen;

    if (free >= addlen) return s;
    len = sdslen(s);   //獲取當前字符串長度
    sh = (void*) (s-(sizeof(struct sdshdr)));   //獲取當前的字符串對象
    newlen = (len+addlen);    //計算擴展後的字符串長度
    //一下爲重點:申請字符串會計算,新的長度是否會超過SDS_MAX_PREALLOC(定義在src/sds.h中,默認爲1M)
    //若是超過則申請SDS_MAX_PREALLOC大小的內存,不然申請2*擴展後字符串的長度
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);  //從新分配內存
    if (newsh == NULL) return NULL;

    newsh->free = newlen - len;  //更新剩餘空間
    return newsh->buf;
}

如上代碼所述,系統在擴展內存的時候,會申請新字符串長度的兩倍,這樣後續在進行追加操做的時候就不進行內存分配處理了,節省了不少內存分配的消耗,固然這樣可能會對內存形成一些浪費,Redis的一些配置能夠改變這種行爲,能夠經過字符串函數 sdsRemoveFreeSpace() 對多申請的那部份內存進行釋放。


更多字符串函數能夠參考個人另一篇文章:

Redis 2.8.9字符串操做函數頭整理,並註釋做用和參數說明


參考資料:

Redis2.8.9源碼   src/sds.h   src/sds.c

Redis 設計與實現(初版)


很是感謝 Redis 設計與實現的做者,給我看代碼帶來了很大的方便。

相關文章
相關標籤/搜索