1. 程式人生 > 資料庫 >redis6.0原始碼學習(二)sds

redis6.0原始碼學習(二)sds

redis6.0原始碼學習(二)sds

文章目錄

1、資料結構

原始碼所在檔案 sds.h 和 sds.c

sds的定義

typedef char *sds;

sds字串根據字串的長度,劃分了五種結構體sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,分別對應的型別為SDS_TYPE_5、SDS_TYPE_8、SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64

在這裡插入圖片描述

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 低3位用來儲存型別,高5位用來儲存長度 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 字串在buf中實際佔用的位元組數(不包括'\0')*/
    uint8_t alloc; /* 去除頭長度和結束符'\0'後的總長度 */
    unsigned char flags;  /* 低位的3個bit位用來表示結構型別,其餘5個bit位未使用 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* 字串在buf中實際佔用的位元組數(不包括'\0')*/
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 低位的3個bit位用來表示結構型別,其餘5個bit位未使用 */
    char buf[];
};
......
  • __attribute__ ((__packed__)) 告訴編譯分配的是緊湊記憶體,而不是位元組對齊的方式。
  • len表示字串已使用的長度,buf長度
  • alloc表示字串的容量
  • flags表示字串型別標記SDS_TYPE_5、SDS_TYPE_8、SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64
  • buf[]表示柔性陣列。在分配記憶體的時候會指向字串的內容

2、sds建立

根據傳入的字串建立sds

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

下面是主要建立的邏輯

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    //根據字串長度獲取SDS的型別
    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 (sh == NULL) return NULL;
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
        memset(sh, 0, hdrlen+initlen+1);
    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); //使用巨集來獲取sdshdr結構體指標
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
		//......省略部分程式碼
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s; //返回值的首地址是buf地址,而不是整個sds的首地址
}

總結步驟就是:

  • 1、根據傳入的字串長度獲取sds的型別(用適當的型別儲存,減少記憶體消耗)。如果初始化長度initlen為0,通常被認為要進行append操作,直接設定SDS型別為SDS_TYPE_8。
  • 2、分配記憶體,大小為:hdrlen+initlen+1 (hdrlen:型別結構體的大小,initlen:字串大小, 1:\0 結束符的長度)
  • 3、初始化sds結構體中alloc、len、 flag值
  • 4、拷貝字串到sds結構體, 新增結束符

3、sds擴容

擴容

/* Enlarge the free space at the end of the sds string so that the caller
 * is sure that after calling this function can overwrite up to addlen
 * bytes after the end of the string, plus one more byte for nul term.
 *
 * Note: this does not change the *length* of the sds string as returned
 * by sdslen(), but only the free buffer space we have. */
sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    size_t avail = sdsavail(s); //獲取sds目前空餘的空間
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK; //獲取sds型別
    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)  //新的長度小於1024*1024,即兩倍的擴容
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC; //新長度直接加上1024*1024大小

    type = sdsReqType(newlen); //獲取儲存新sds的結構體型別

    /* 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) {
        //sds型別不變,重新分配記憶體
        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 */
        //sds型別發生改變,重新申請新記憶體
        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); //更新sds已使用空間長度
    }
    sdssetalloc(s, newlen);//更新sds容量
    return s;
}

步驟大致如下:

  • 1、檢視sds中是否有足夠的剩餘空間容納addlen長度的字串,有則返回,無則繼續其它操作。
  • 2、 計算需要重新分配的儲存空間的長度,包括原sds長度與addlen,另外預備一部分的剩餘空間。
  • 3、如果新sds長度小於1M則預設兩倍擴容,否則只 擴容 (1M + addlen) 大小
  • 4、根據新的長度,得到新的sds頭部型別,如果新的頭部型別與原型別相同,則使用s_realloc分配更多的空間;如果新的頭部型別與原型別不相同,則使用s_alloc重新分配記憶體,並將原sds內容copy到新分配的空間。

4、sds縮容

sds在縮容時,並不是立即釋放記憶體。比如sdstrim函式

/* Remove the part of the string from left and from right composed just of
 * contiguous characters found in 'cset', that is a null terminted C string.
 *
 * After the call, the modified sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call.
 *
 * Example:
 *
 * s = sdsnew("AA...AA.a.aa.aHelloWorld     :::");
 * s = sdstrim(s,"Aa. :");
 * printf("%s\n", s);
 *
 * Output will be just "Hello World".
 *從 SDS 左右兩端分別移除所有在 C 字串中出現過的字元。

 *接受一個 SDS 和一個 C 字串作為引數, 從 SDS 左右兩端分別移除所有在 C 字串中出現過的字元。
 */
sds sdstrim(sds s, const char *cset) {
    char *start, *end, *sp, *ep;
    size_t len;

    sp = start = s;
    ep = end = s+sdslen(s)-1;
    while(sp <= end && strchr(cset, *sp)) sp++;
    while(ep > sp && strchr(cset, *ep)) ep--;
    len = (sp > ep) ? 0 : ((ep-sp)+1);
    if (s != sp) memmove(s, sp, len);
    s[len] = '\0';
    sdssetlen(s,len);
    return s;
}

比如有個字串s1=“REDIS”,對s1進行sdstrim(s1," S")操作,執行完該操作之後Redis不會立即回收減少的部分,也就是而是會分配給下一個需要記憶體的程式。
下面的函式是真正釋放記憶體。
在這裡插入圖片描述

/* Reallocate the sds string so that it has no free space at the end. The
 * contained string remains not altered, but next concatenation operations
 * will require a reallocation.
 *
 * After the call, the passed sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call. */
sds sdsRemoveFreeSpace(sds s) {
    void *sh, *newsh; 
    char type, oldtype = s[-1] & SDS_TYPE_MASK; //獲取sds型別
    int hdrlen, oldhdrlen = sdsHdrSize(oldtype); //獲取sds頭的長度
    size_t len = sdslen(s);
    size_t avail = sdsavail(s); //獲取sds目前空餘的空間
    sh = (char*)s-oldhdrlen;

    /* Return ASAP if there is no space left. */
    if (avail == 0) return s;

    /* Check what would be the minimum SDS header that is just good enough to
     * fit this string. */
    type = sdsReqType(len);
    hdrlen = sdsHdrSize(type);

    /* If the type is the same, or at least a large enough type is still
     * required, we just realloc(), letting the allocator to do the copy
     * only if really needed. Otherwise if the change is huge, we manually
     * reallocate the string to use the different header type. */
    if (oldtype==type || type > SDS_TYPE_8) {
        newsh = s_realloc(sh, oldhdrlen+len+1);
        if (newsh == NULL) return NULL;
        s = (char*)newsh+oldhdrlen;
    } else {
        newsh = s_malloc(hdrlen+len+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); //更新sds已使用空間長度
    }
    sdssetalloc(s, len);//更新sds容量
    return s;
}

和擴容的步驟基本相似。

5、總結

SDS和C字串的區別

(1)獲取字串長度的時間複雜度
C字串不記錄字串本身是長度,因此需要遍歷字串才能得到字串的長度,時間複雜度為O(N),SDS字串用屬性len記錄了字串的長度,因此獲取字串長度的時間複雜度為O(1)。
(2)緩衝區溢位
如果對C字串使用strcat進行拼接,如果沒有提前對字串分配足夠的記憶體,則會導致緩衝區溢位。但是SDS會提前對字串所需要的記憶體進行檢測。
(3)修改字串時的記憶體重分配
當修改C字串的時候,無論是增長/縮短字串,都會通過記憶體重分配來擴充套件或者釋放記憶體,否則就會導致緩衝區溢位或者記憶體洩漏。SDS在擴容是採用的是預分配機制避免了頻繁擴容。
(4)二進位制安全性
如果一個字串儲存的時候是什麼樣子,輸出的時候也是什麼樣子,則稱為二進位制安全的,C字串以’\0’作為字串的結尾標識,會造成從中間截斷字串的情況。SDS使用len屬性記錄字串的長度,因此儲存二進位制資料是安全的。也就是說sds中間也可以存在 \0 字元。