Redis SDS實現


 

介紹

Redis沒有直接使用C語言傳統的字符串而是自己創建了一種名為簡單動態字符串SDS(simple dynamic string)的抽象類型(C語言封裝的字符串類型),並將SDS用作Redis的默認字符串表示。

SDS是Redis默認的字符表示,比如包含字符串值的鍵值對都是由SDS實現的。

sds 有兩個版本,在Redis 3.2之前使用的是第一個版本,其數據結構如下所示:

typedef char *sds;      //注意,sds其實不是一個結構體類型,而是被typedef的char*

struct sdshdr {
    unsigned int len;   //buf中已經使用的長度
    unsigned int free;  //buf中未使用的長度
    char buf[];         //柔性數組buf
};

但是在Redis 3.2 版本中,對數據結構做出了修改,針對不同的長度范圍定義了不同的結構,如下,這是目前的結構:

typedef char *sds;      

struct __attribute__ ((__packed__)) sdshdr5 {     // 對應的字符串長度小於 1<<5
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {     // 對應的字符串長度小於 1<<8
    uint8_t len; /* used */                       //目前字符創的長度
    uint8_t alloc;                                //已經分配的總長度
    unsigned char flags;                          //flag用3bit來標明類型,類型后續解釋,其余5bit目前沒有使用
    char buf[];                                   //字符數組,以'\0'結尾
};
struct __attribute__ ((__packed__)) sdshdr16 {    // 對應的字符串長度小於 1<<16
    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 {    // 對應的字符串長度小於 1<<32
    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 {    // 對應的字符串長度小於 1<<64
    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[];
};

為了滿足不同長度的字符串可以使用不同大小的Header,從而節省內存,可以選取不同的數據類型uint8_t或者uint16_t或者uint32_t等來表示長度、一共申請字節的大小等。

上面結構體中的__attribute__ ((__packed__)) 設置是告訴編譯器取消字節對齊,則結構體的大小就是按照結構體成員實際大小相加得到的。

 

3.2版本之前的SDS

 

 

 注意:這里的len是buf字符數組中,不包括最后的空字符的字符個數。

 

另外帶有空閑空間的SDS字符串例子:

 

 

 

 

Redis SDS與C語言比較

1)獲取字符串長度的時間復雜度

SDS獲取字符串長度:O(1)。

C字符串獲取字符串長度時間復雜度為O(N),需要遍歷字符串,以空字符為結尾。

使用SDS可以確保獲取字符串長度的操作不會成為Redis的性能瓶頸。

2)杜絕緩沖區溢出

C字符串不記錄自身長度和空閑空間,容易造成緩沖區溢出,使用SDS則不會,SDS拼接字符串之前會先通過free字段檢測剩余空間能否滿足需求,不能滿足需求的就會擴容。

3)減少修改字符串時帶來的內存重分配次數

使用C字符串的話:

每次對一個C字符串進行增長或縮短操作,長度都需要對這個C字符串數組進行一次內存重分配,比如C字符串的拼接,程序要先進行內存重分配來擴展字符串數組的大小,避免緩沖區溢出,又比如C字符串的縮短操作,程序需要通過內存重分配來釋放不再使用的那部分空間,避免內存泄漏,所以C語言中每次修改字符串都會造成內存重分配。

使用SDS的話:

通過SDS的len屬性和free屬性可以實現兩種內存分配的優化策略:空間預分配和惰性空間釋放。

1.針對內存分配的策略:空間預分配(SDS字符串擴容操作)

在對SDS的空間進行擴展的時候,程序不僅會為SDS分配修改所必須的空間,還會為SDS分配額外的未使用的空間

這樣可以減少連續執行字符串增長操作所需的內存重分配次數,通過這種預分配的策略,SDS將連續增長N次字符串所需的內存重分配次數從必定N次降低為最多N次,這是個很大的性能提升。

額外分配未使用空間的大小由以下策略決定:
在擴展sds空間之前,sds api會檢查未使用的空間是否夠用,如果夠用則直接使用未使用的空間,無須執行內存重分配。

如果空間不夠用則執行內存重分配:

2.針對內存釋放的策略:惰性空間釋放(SDS字符串縮短操作)

在對SDS的字符串進行縮短操作的時候,程序並不會立刻使用內存重分配來回收縮短之后多出來的字節,而是使用free屬性將這些字節的數量記錄下來等待將來使用。

通過惰性空間釋放策略,SDS避免了縮短字符串時所需的內存重分配次數,並且為將來可能有的增長操作提供了優化!

當然如果我們在有需要的時候,也可以通過sds api來釋放未使用的空間,不用擔心惰性空間釋放策略會造成內存浪費。

4)二進制安全

為了確保數據庫可以二進制數據(圖片,視頻等),SDS的API都是二進制安全的,所有的API都會以處理二進制的方式來處理存放在SDS的buf數組里面的數據,程序不會對其中的數據做任何的限制,過濾,數據存進去是什么樣子,讀出來就是什么樣子,這也是buf數組叫做字節數組而不是叫字符數組的原因,以為它是用來保存一系列二進制數據的。

通過二進制安全的SDS,Redis不僅可以保存文本數據,還可以保存任意格式是二進制數。

而C語言字符串的字符必須符號某種編碼(比如ascii),並且除了末尾的空字符,字符串其他位置不能包含空字符,所以C語言字符串只能保存文本數據,不能保存二進制數據。

5)兼容部分c語言函數

總結:

3.2版本前sds sapi源碼

Redis學習之SDS源碼分析

 

3.2版本之后的SDS

下面內容轉載:Redis源碼分析(sds)

此時的SDS由兩個部分組成,Header與數據部分。

Header部分主要包含以下幾個部分:

  • len:表示字符串真正的長度,不包括空終止字符
  • alloc:表示字符串的最大容量,不包含Header和最后的空終止字符
  • flags:表示Header的類型

數據部分:字符數組。

 

 

 

由於sds的header共有五種,要想得到sds的header屬性,就必須先知道header的類型,flags字段存儲了header的類型。假如我們定義了sds* s,那么獲取flags字段僅僅需要將s向前移動一個字節,即unsigned char flags = s[-1]。

// 五種header類型,flags取值為0~4
#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

然后通過以下宏定義來對header進行操作

#define SDS_TYPE_MASK 7 // 類型掩碼
#define SDS_TYPE_BITS 3 
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); // 獲取header頭指針
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) // 獲取header頭指針
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS) // 獲取sdshdr5的長度

 

創建、擴容和銷毀

接下來我們以一個例子來跟蹤源碼展示sds的創建、擴容和銷毀等過程,這是我們的源代碼:

int main(int argc, char *argv[]) {
    sds s = sdsnew("Hello World,");
    printf("Length:%d, Type:%d\n", sdslen(s), sdsReqType(sdslen(s)));

    s = sdscat(s, "The length of this sentence is greater than 32 bytes");
    printf("Length:%d, Type:%d\n", sdslen(s), sdsReqType(sdslen(s)));

    sdsfree(s);
    return 0;
}

Out>
Length:12, Type:0
Length:64, Type:1

首先我們創建了一個sds名為s,初始化為”Hello World”,然后打印它的length和type分別為12和0,接着我們繼續給s追加了一個字符串,使得它的長度變成了64,獲取type,發現變成了1,最后free掉s,有關type的定義,位於sds.h頭文件,隨着長度不同,type也會發生變化。

#define SDS_TYPE_5  0     //長度小於 1<<5 即32,類型為SDS_TYPE_5
#define SDS_TYPE_8  1     // ...
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4

下面我們從sdsnew出發,去看下它的實現:

/* Create a new sds string starting from a null terminated C string. */
sds sdsnew(const char *init) {
    size_t initlen = (init == NULL) ? 0 : strlen(init);
    return sdsnewlen(init, initlen);
}

可以看到sdsnew實際上調用了sdsnewlen,幫我們計算了傳進去的字符串長度,然后傳給sdsnewlen,繼續看sdsnewlen

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);
    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;
}

 

函數基本流程如下所示:

char type = sdsReqType(initlen);根據我們傳入的初始化字符串長度獲取類型,獲取代碼如下:

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;
}


函數根據字符串大小的不同返回不同的類型。

int hdrlen = sdsHdrSize(type);根據上一步獲取的type通過sdsHdrSize函數獲得Header的長度,sdsHdrSize代碼如下:

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;
}

 


這個函數直接return了相應的結構體大小。

接下來malloc申請了hdrlen+initlen+1大小的空間,表示頭部+字符串+Null,然后讓s指向了字符串的首地址,fp指向了頭部的最后一個字節,也就是flag。

然后我們的程序進入了switch,因為類型為SDS_TYPE_5,所以執行了*fp = type | (initlen << SDS_TYPE_BITS); 對於SDS_TYPE_5類型來說,長度信息實際上也是存在flag里面的,因為最大長度是31,占5bit,還有3bit表示type。

接着break出來后,完成了字符串的拷貝工作,然后給s結尾置’\0’,s[initlen] = '\0';,至此,sdsnew調用完畢,此時我們的sds結構如下圖所示:

 

flag大小為1字節,中間的String長度為11字節,后面還有一個\0結尾。接着我們的代碼執行輸出長度和類型,然后調用了sdscat函數,如下:

s = sdscat(s, "The length of this sentence is greater than 32 bytes");


我們給原始的s繼續追加了超過32個字符,其實目的是為了是它轉變成SDS_TYPE_8類型,sdscat的代碼如下所示:

sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}

它調用了sdscatlen函數:

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);
s[curlen+len] = '\0';
return s;
}

size_t curlen = sdslen(s);首先獲取了當前的長度curlen,接着調用了sdsMakeRoomFor函數,這個函數比較關鍵,它能保證s的空間足夠,如果空間不足會動態分配,代碼如下:

 

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;
    int hdrlen;

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

    len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    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);
        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);
        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;
}

size_t avail = sdsavail(s);首先調用sdsavail函數獲取了當前s可用空間的大小,sdsavail函數如下:

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;
}


對於SDS_TYPE_5類型,直接return 0,對於其他類型,需要在Header獲取alloc和len然后相減,獲取Header的宏如下:

SDS_HDR_VAR(8,s);

#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));

//本質上就是用s的地址減去(偏移)相應頭部結構體大小的地址,就到了Header的第一個字節

return sh->alloc - sh->len;

//然后返回可用字節大小

if (avail >= addlen) return s; 接着判斷大小,如果空間是足夠的,則將s返回,函數結束。
否則我們獲取到目前的長度,然后給它加上sdscat所追加的字符串長度,如果此時的新長度沒有超過SDS_MAX_PREALLOC=1024*1024,我們再給新長度x2,這樣做是為了避免頻繁調用malloc。
type = sdsReqType(newlen); 然后我們需要根據新長度重新獲取type類型。
if (oldtype==type)然后判斷type是否發生了變化,來決定擴充空間還是重新申請空間。對於我們的例子,接下來需要重新分配空間,如下,走else分支:

else {
        /* Since the header size changes, need to move the string forward,
         * and can't use realloc */
        newsh = s_malloc(hdrlen+newlen+1);      //重新分配Header+newlen+1的空間
        if (newsh == NULL) return NULL;
        memcpy((char*)newsh+hdrlen, s, len+1);  //將String部分拷貝至新String部分
        s_free(sh);                             //把舊的sds全部釋放
        s = (char*)newsh+hdrlen;                
        s[-1] = type;                           //將type更新
        sdssetlen(s, len);                      //設置大小
    }
    sdssetalloc(s, newlen);                     //設置alloc大小
    return s;                                   //將新的s返回
}

當sdsMakeRoomFor函數返回后,sdscatlen函數繼續執行,將需要添加的字符串拷貝至新的空間,然后設置長度和最后的\0就返回了。此時s變成了下面這樣:

 

需要注意的是執行代碼打印出來長度為64指的是已經分配的長度,也就是len的大小,圖片上的128是alloc的大小,則此時可用長度還有64字節,下次如果再追加小於64字節的內容就不會重新分配了。最后我們看下free的過程,代碼如下:

void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1]));
}

很簡單,如果為NULL就返回,否則得到Header的首地址然后釋放,sdsHdrSize(s[-1])是根據flag類型獲取Header的長度,用s減去(偏移)Header長度個字節就到頭部了。上面的過程基本上分析清楚了sds有關於創建和擴容以及釋放的過程,這樣其實已經把握了sds的大體脈絡。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM