1. 程式人生 > >redis原始碼分析與思考(一)——sds

redis原始碼分析與思考(一)——sds

  在閱讀黃健巨集的書《Redis設計與實現》的時候,深刻的意識到僅僅看別人的作品是遠遠不夠,自己更應該去閱讀原始碼,形成自己的思考,這樣才算真正的學進去了。

  現如今,Nosql的概念大行其道,redis作為其中的佼佼者被廣大的開發者愛好著,而且Redis的原始碼僅僅只有三萬多行,作為一名喜愛開源技術以及新技術的人來說,Redis原始碼無疑是值得每個開發者閱讀的。

  在Redis中,即使原始碼是由c寫成的,但是卻沒有使用c字串,而是封裝了一個sds字串,在redis原始碼中對應著sds.c與sds.h兩個檔案,其sds字串結構體定義如下:

struct sdshdr {
    // buf 中已佔用空間的長度
    int len;
    // buf 中剩餘可用空間的長度
    int free;
    // 資料空間
    char buf[];
};

  len與free來記錄所存資料(c字串)的長度也即是佔據buf空間的大小與buf空間剩餘的多少。

  現列出sds的函式呼叫:

//建立一個新的指定initlen大小buf的sdshdr字串
sds sdsnewlen(const void *init, size_t initlen);
//建立一個不指定buf空間大小的sdshdr字串,呼叫sdsnewlen
sds sdsnew(const char *init);
//建立了儲存“”的sdshdr
sds sdsempty(void);
//返回buf空間已用長度
size_t sdslen(const sds s);
//複製並返回一個sdshdr
sds sdsdup(const sds s);
//銷燬一個sdshdr
void sdsfree(sds s);
//返回buf空間空餘的長度
size_t sdsavail(const sds s);
//增長sds至指定長度,並初始化為0
sds sdsgrowzero(sds s, size_t len);
//將額外字串與原buf拼接起來
sds sdscatlen(sds s, const void *t, size_t len);
//呼叫sdscatlen
sds sdscat(sds s, const char *t);
//與sdscat功能一致,傳遞引數不一樣
sds sdscatsds(sds s, const sds t);
//複製指定長度的字串
sds sdscpylen(sds s, const char *t, size_t len);
//呼叫sdscpylen
sds sdscpy(sds s, const char *t);
/**
*輸出函式
**/
sds sdscatvprintf(sds s, const char *fmt, va_list ap);
#ifdef __GNUC__
sds sdscatprintf(sds s, const char *fmt, ...)
    __attribute__((format(printf, 2, 3)));
#else
sds sdscatprintf(sds s, const char *fmt, ...);
#endif

sds sdscatfmt(sds s, char const *fmt, ...);
//消除指定的字元
sds sdstrim(sds s, const char *cset);
//擷取指定開始結尾的字串
void sdsrange(sds s, int start, int end);
//沒有被使用
void sdsupdatelen(sds s);
//清空sdshdr
void sdsclear(sds s);
//比較長度大小
int sdscmp(const sds s1, const sds s2);
//使用分隔符進行分割,
sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count);
//
void sdsfreesplitres(sds *tokens, int count);
//轉化為小寫
void sdstolower(sds s);
//轉化為大寫
void sdstoupper(sds s);
//用long long型別來創造一個sds
sds sdsfromlonglong(long long value);
//將長度為 len 的字串 p 以帶引號(quoted)的格式追加到給定 sds 的末尾
sds sdscatrepr(sds s, const char *p, size_t len);
//
sds *sdssplitargs(const char *line, int *argc);
//替換指定的字元
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);
//
sds sdsjoin(char **argv, int argc, char *sep);

/* 暴露給使用者的低等級的api */
//增加sdshdr的buf的長度,成倍增加
sds sdsMakeRoomFor(sds s, size_t addlen);
//根據incr來增加sds空間
void sdsIncrLen(sds s, int incr);
//移除sdshdr多餘的空間
sds sdsRemoveFreeSpace(sds s);
//返回給定 sdshdr 分配的記憶體位元組數
size_t sdsAllocSize(sds s);

  挑選其中一些重要的點來談談自己的理解,首先,在sds.h中有兩個靜態的行內函數,也就是sdslen與sdsavail,先看其原始碼(ps:下文若無說明sds即為sdshdr):

typedef char *sds;

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

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

  這兩個函式本身的含義十分的簡單,一個是獲取sds字串的長度,另一個則是返回sds中未使用的空間,但是其中巧妙的利用連續地址實現了sds與普通字串的轉換,也就是這句程式碼:

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

  根據c的記憶體分配可知,結構體內的資料空間是連續的,利用字串s的首地址減去一個結構體的佔用記憶體的大小,也就是把s的首地址往前移了sizeof(struct sdshdr)的大小,並且對其實行強制轉換,使其成為一個無型別的指標,再將sds的指標指向s的首地址,完成轉換。

  其實sizeof(struct sdshdr)的大小就是sizeof(int len)+sizeof(int free),即八個位元組,因為buf資料尚未賦值與初始化,所以它所佔記憶體為零,如下程式碼所示:

#include <iostream>
struct test{
    int b;
    char c[];
};
int main() {
    std::cout << "Hello, World!" << std::endl;
    //arrayList arrayList1<int>(10);
     struct test c;
    std::cout<< sizeof(c);
    return 0;
}

  執行結果為:

  可見只包含了一個int的大小,所以s首地址只向前移動了八個位元組,s字串往前移動的八個位元組加上其原來本身的字串的資料,恰好的構成了一個sds結構體記憶體的分配規則,從而可以完成轉換。

  如圖所示:

  再來看看sds是如何建立一個新的sds的:

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;
   //如果init不為空
    if (init) {
   //申請不初始化的malloc空間
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    } else {
   //申請初始化為零的calloc空間
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
    }
   //申請記憶體失敗,返回空
    if (sh == NULL) return NULL;
  //將長度賦值給len
    sh->len = initlen;
    sh->free = 0;
    //如果initlen與init同時不為假,進行copy
    if (initlen && init)
        memcpy(sh->buf, init, initlen);
  //將字串末尾設為'\0'
    sh->buf[initlen] = '\0';
    return (char*)sh->buf;
}

    而之所以申請記憶體要加1,是因為字串結尾處需要'\0'來結尾,'\0'恰好是一個位元,而zmalloc與zcalloc是redis自己封裝了malloc與calloc兩個函式,程式碼如下:

#define PREFIX_SIZE (sizeof(size_t))

void *zmalloc(size_t size) {
    //申請記憶體
    void *ptr = malloc(size+PREFIX_SIZE);
  //異常處理
    if (!ptr) zmalloc_oom_handler(size);
  //記憶體統計
    *((size_t*)ptr) = size;
    update_zmalloc_stat_alloc(size+PREFIX_SIZE);
    return (char*)ptr+PREFIX_SIZE;
}

  申請記憶體的時候加上PREFIX_SIZE,是由於redis劃分出來一部分記憶體用來儲存該個結構體的總體佔用記憶體大小,具體在這不談,後面會用一篇來談談redis對記憶體的管理。

  那麼,sds記憶體不夠的時候怎麼擴充自己的記憶體的呢?利用好了free這個屬性,對空間進行預分配,當需要增加的記憶體大小小於free時,便不擴充,使用原有的記憶體,當其大於時,便擴充。

#define SDS_MAX_PREALLOC (1024*1024)//預設最大分配值

sds sdsMakeRoomFor(sds s, size_t addlen) {

    struct sdshdr *sh, *newsh;
    // 獲取 s 目前的空餘空間長度
    size_t free = sdsavail(s);
    size_t len, newlen;

    // s 目前的空餘空間已經足夠,無須擴充套件
    if (free >= addlen) return s;

    // 獲取 s 目前已佔用空間的長度
    len = sdslen(s);
    sh = (void*) (s-(sizeof(struct sdshdr)));

    // s 最少需要的長度
    newlen = (len+addlen);

    // 根據新長度,為 s 分配新空間所需的大小
    if (newlen < SDS_MAX_PREALLOC)
        // 如果新長度小於 SDS_MAX_PREALLOC 
        // 那麼為它分配兩倍於所需長度的空間
        newlen *= 2;
    else
        // 否則,分配長度為目前長度加上 SDS_MAX_PREALLOC
        newlen += SDS_MAX_PREALLOC;
   
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);

    // 記憶體不足,分配失敗,返回
    if (newsh == NULL) return NULL;

    // 更新 sds 的空餘長度
    newsh->free = newlen - len;

    // 返回 sds
    return newsh->buf;
}

  而sds也提供了回收記憶體的函式

sds sdsRemoveFreeSpace(sds s) {
    struct sdshdr *sh;
    sh = (void*) (s-(sizeof(struct sdshdr)));
    sh = zrealloc(sh, sizeof(struct sdshdr)+sh->len+1);
    sh->free = 0;
    return sh->buf;
}

  該函式是用於當機器記憶體不夠時,對sds進行記憶體回收,但不影響字串的儲存,只是將空餘的空間給回收了,也即是重新分配了記憶體空間。

  sds相比較普通的字串有如下的優點:

  1、杜絕了緩衝區的溢位,實現了動態的儲存,惰性釋放記憶體空間

  2、獲取其長度的時間複雜度為O(1),動態的分配其記憶體空間