1. 程式人生 > 其它 >redis原始碼閱讀—sds

redis原始碼閱讀—sds

技術標籤:redissds

文章目錄

sdshdr定義

真實的資料並不是直接儲存,也有封裝,看下面的程式碼就知道分為五種,分別是sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64。sdshdr5和另外四種的區別比較明顯,sdshrd5其實對記憶體空間的更加節約。包括已用長度len,總長度alloc(分配的長度),標記flags,實際資料buf。

redis設計和實現

—3.0版本的定義

struct sdshdr {
// 記錄 buf 陣列中已使用位元組的數量
// 等於 SDS 所儲存字串的長度
int len;
// 記錄 buf 陣列中未使用位元組的數量
int free;
// 位元組陣列,用於儲存字串
char buf[];
};
表示示例


原始碼

// sds頭定義的地方

/* 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[];
};

這部分原始碼沒啥特別,就不多說了

sds邏輯圖

假設我們設定某個字串為hello,那麼他SDS的可用長度len為8,已用長度len為6,如下圖。注意:Redis會根據具體的字元長度,選擇相應的sdshdr,但是各個型別都差不多,所以下圖加簡單畫了。
在這裡插入圖片描述

sds優勢

更快的獲取字串長度

在開發語言中,一般會提供方法直接獲取大小。但是C卻不一樣,更偏向底層實現,所以沒有直接的方法使用。這樣就帶來一個問題,如果我們想要獲取某個陣列的長度,就只能從頭開始遍歷,當遇到第一個’\0’則表示該陣列結束。這樣的速度太慢了,不能每次因為要獲取長度就變數陣列。所以設計了SDS資料結構,在原來的字元陣列外面增加總長度,和已用長度,這樣每次直接獲取已用長度即可。複雜度為O(1)。

通過使用 SDS 而不是 C 字串, Redis 將獲取字串長度所需的複雜度從 O(N) 降低到了 O(1) , 這確保了獲取字串長度的工作不會成為 Redis 的效能瓶頸。比如說, 因為字串鍵在底層使用 SDS 來實現, 所以即使我們對一個非常長的字串鍵反覆執行 STRLEN 命令, 也不會對系統性能造成任何影響, 因為 STRLEN 命令的複雜度僅為 O(1) 。

杜絕緩衝區溢位

SDS 的空間分配策略完全杜絕了發生緩衝區溢位的可能性: 當 SDS API 需要對 SDS 進行修改時, API 會先檢查 SDS 的空間是否滿足修改所需的要求, 如果不滿足的話, API 會自動將 SDS 的空間擴充套件至執行修改所需的大小, 然後才執行實際的修改操作, 所以使用 SDS 既不需要手動修改 SDS 的空間大小, 也不會出現前面所說的緩衝區溢位問題。

舉個例子, SDS 的 API 裡面也有一個用於執行拼接操作的 sdscat 函式, 它可以將一個 C 字串拼接到給定 SDS 所儲存的字串的後面, 但是在執行拼接操作之前, sdscat 會先檢查給定 SDS 的空間是否足夠, 如果不夠的話, sdscat 就會先擴充套件 SDS 的空間, 然後才執行拼接操作。

減少修改字串時帶來的記憶體重分配次數

  • 空間預分配

    空間預分配用於優化 SDS 的字串增長操作: 當 SDS 的 API 對一個 SDS 進行修改, 並且需要對 SDS 進行空間擴充套件的時候, 程式不僅會為 SDS 分配修改所必須要的空間, 還會為 SDS 分配額外的未使用空間。

    其中, 額外分配的未使用空間數量由以下公式決定:

    • 如果對 SDS 進行修改之後, SDS 的長度(也即是 len 屬性的值)將小於 1 MB , 那麼程式分配和 len 屬性同樣大小的未使用空間, 這時 SDS len 屬性的值將和 free 屬性的值相同。 舉個例子, 如果進行修改之後, SDS 的 len 將變成 13 位元組, 那麼程式也會分配 13 位元組的未使用空間, SDS 的 buf 陣列的實際長度將變成 13 + 13 + 1 = 27 位元組(額外的一位元組用於儲存空字元)。
    • 如果對 SDS 進行修改之後, SDS 的長度將大於等於 1 MB , 那麼程式會分配 1 MB 的未使用空間。 舉個例子, 如果進行修改之後, SDS 的 len 將變成 30 MB , 那麼程式會分配 1 MB 的未使用空間, SDS 的 buf 陣列的實際長度將為 30 MB + 1 MB + 1 byte 。

    通過空間預分配策略, Redis 可以減少連續執行字串增長操作所需的記憶體重分配次數。

  • 惰性空間釋放

    惰性空間釋放用於優化 SDS 的字串縮短操作: 當 SDS 的 API 需要縮短 SDS 儲存的字串時, 程式並不立即使用記憶體重分配來回收縮短後多出來的位元組, 而是使用 free 屬性將這些位元組的數量記錄起來, 並等待將來使用。

二進位制安全

如果傳統字串儲存圖片,視訊等二進位制檔案,中間可能出現’\0’,如果按照原來的邏輯,會造成資料丟失。所以可以用已用長度來表示是否字元陣列已結束。

關鍵程式碼

獲取常見值

在sds.h中寫了一些常見方法,比如計算sds的長度(即sdshdr的len),計算sds的空閒長度(即sdshdr的可用長度alloc-已用長度len),計算sds的可用長度(即sdshdr的alloc)等等。但是大家有沒有疑問,這不是一行程式碼搞定的事嗎,為啥要抽象出方法呢?那麼問題在於在上面,我們有將sdshdr分為五種型別,分別是sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64。那麼我們在實際使用的時候,想要區分當前是哪個型別,並取其相應欄位或設定相應欄位。

//計算sds對應的字串長度,其實上取得是字串所對應的哪種sdshdr的len值 
static inline size_t sdslen(const sds s) {
	// 柔性陣列不佔空間,所以倒數第二位的是flags 
    unsigned char flags = s[-1];
    //flags與上面定義的巨集變數7做位運算 
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5://0 
            return SDS_TYPE_5_LEN(flags);
        case SDS_TYPE_8://1
            return SDS_HDR(8,s)->len;//取上面結構體sdshdr8的len  
        case SDS_TYPE_16://2
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32://3
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64://5
            return SDS_HDR(64,s)->len;
    }
    return 0;
}
 
//計算sds對應的空餘長度,其實上是alloc-len 
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;
}
 
//設定sdshdr的len 
static inline void sdssetlen(sds s, size_t newlen) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            {
                unsigned char *fp = ((unsigned char*)s)-1;
                *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
            }
            break;
        case SDS_TYPE_8:
            SDS_HDR(8,s)->len = newlen;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->len = newlen;
            break;
        case SDS_TYPE_32:
            SDS_HDR(32,s)->len = newlen;
            break;
        case SDS_TYPE_64:
            SDS_HDR(64,s)->len = newlen;
            break;
    }
}
 
//給sdshdr的len新增多少大小 
static inline void sdsinclen(sds s, size_t inc) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            {
                unsigned char *fp = ((unsigned char*)s)-1;
                unsigned char newlen = SDS_TYPE_5_LEN(flags)+inc;
                *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
            }
            break;
        case SDS_TYPE_8:
            SDS_HDR(8,s)->len += inc;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->len += inc;
            break;
        case SDS_TYPE_32:
            SDS_HDR(32,s)->len += inc;
            break;
        case SDS_TYPE_64:
            SDS_HDR(64,s)->len += inc;
            break;
    }
}
 
//獲取sdshdr的總長度 
static inline size_t sdsalloc(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)->alloc;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->alloc;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->alloc;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->alloc;
    }
    return 0;
}
 
//設定sdshdr的總長度 
static inline void sdssetalloc(sds s, size_t newlen) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            /* Nothing to do, this type has no total allocation info. */
            break;
        case SDS_TYPE_8:
            SDS_HDR(8,s)->alloc = newlen;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->alloc = newlen;
            break;
        case SDS_TYPE_32:
            SDS_HDR(32,s)->alloc = newlen;
            break;
        case SDS_TYPE_64:
            SDS_HDR(64,s)->alloc = newlen;
            break;
    }
}

建立物件

我們通過sdsnew方法來建立物件,顯示通過判斷init是否為空來確定初始大小,接著呼叫方法sdsnew(這邊方法名一樣,但是引數不一樣,其為方法的過載),先根據長度確定型別(上面有提過五種型別,不記得的可以往上翻),然後根據型別分配相應的記憶體資源,最後追加C語言的結尾符’\0’。

sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    return sdsnewlen(init, initlen);
}
 
 
sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);//根據長度確定型別 
    /*空字串,用sdshdr8,這邊是經驗寫法,當想構造空串是為了放入超過32長度的字串 */
    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)
        memset(sh, 0, hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    //根據不同的型別,建立不同結構體,呼叫SDS_HDR_VAR函式
	//為不同的結構體賦值,如已用長度len,總長度alloc 
    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);
    //最後追加'\0' 
    s[initlen] = '\0';
    return s;
}
 
 
//根據實際字元長度確定型別 
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;
#endif
    return SDS_TYPE_64;
}
刪除
String型別的刪除並不是直接回收記憶體,而是修改字元,讓其為空字元,這其實是惰性釋放,等待將來使用。在呼叫sdsempty方法時,再次呼叫上面的sdsnewlen方法。
 
/*修改sds字串使其為空(零長度)。
*但是,所有現有緩衝區不會被丟棄,而是設定為可用空間
*這樣,下一個append操作將不需要分配到
*當要縮短SDS儲存的字串時,程式並不立即使用記憶體充分配來回收縮短後多出來的位元組,並等待將來使用。
void sdsclear(sds s) {
    sdssetlen(s, 0);
    s[0] = '\0';
}
sds sdsempty(void) {
    return sdsnewlen("",0);
}

新增字元(擴容)重點!!!

新增字串,sdscat輸入引數為sds和字串t,首先呼叫sdsMakeRoomFor擴容方法,再追加新的字串,最後新增上結尾符’\0’。我們來看下擴容方法裡面是如何實現的?第一步先呼叫常見方法中的sdsavail方法,獲取還剩多少空閒空間。如果空閒空間大於要新增的字串t的長度,則直接返回,不想要擴容。如果空閒空間不夠,則想要擴容。第二步判斷想要擴容多大,這邊有分情況,如果目前的字串小於1M,則直接擴容雙倍,如果目前的字串大於1M,則直接新增1M。第三個判斷新增字串之後的資料型別還是否和原來的一致,如果一致,則沒啥事。如果不一致,則想要新建一個sdshdr,把現有的資料都挪過去。

這樣是不是有點抽象,舉個例子,現在str的字串為hello,目前是sdshdr8,總長度50,已用6,空閒44。現在想要新增長度為50的字元t,第一步想要看下是否要擴容,50明顯大於44,需要擴容。第二步擴容多少,str的長度小於1M,所以擴容雙倍,新的長度為50*2=100。第三步50+50所對應sdshdr型別還是sdshdr8嗎?明顯還是sdshdr8,所以不要資料遷移,還在原來的基礎上新增t即可。

sds sdscat(sds s, const char *t) {
    return sdscatlen(s, t, strlen(t));
}
 
sds sdscatlen(sds s, const void *t, size_t len) {
	//呼叫sds.h裡面的sdslen,即取已用長度 
    size_t curlen = sdslen(s);
	//擴容方法 
    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len);
    sdssetlen(s, curlen+len);
    s[curlen+len] = '\0';
    return s;
}
 
sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    //呼叫sds.h,獲取空閒長度alloc 
    size_t avail = sdsavail(s);
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
 
   //空閒長度大於需要增加的,不需要擴容,直接返回 
    if (avail >= addlen) return s;
 
//呼叫sds.h裡面的sdslen,即取可用長度 
    len = sdslen(s);
    
    sh = (char*)s-sdsHdrSize(oldtype);
    //len加上要新增的大小 
    newlen = (len+addlen);
    
    //#define SDS_MAX_PREALLOC (1024*1024) 
    //當新長度小於 1024*1024,直接擴容兩倍 
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else //當新長度大於 1024*1024,加2014*1024 
        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 {//如果型別不一樣,重新開闢記憶體,把原來的資料複製過去 
        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;
}
 
//計算不同型別的結構體的大小 
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