1. 程式人生 > >Redis5.0原始碼解析(一)----------簡單動態字串(SDS)

Redis5.0原始碼解析(一)----------簡單動態字串(SDS)

基於Redis5.0

Redis 沒有直接使用 C 語言傳統的字串表示(以空字元結尾的字元陣列,以下簡稱 C 字串), 而是自己構建了一種名為簡單動態字串(simple dynamic string,SDS)的抽象型別, 並將 SDS 用作 Redis 的預設字串表示。

SDS定義

//<sds.h>

typedef char *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[]; }; //flag低3位儲存sds型別,sdshdr5的flag高5位儲存buf長度 #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 #define SDS_TYPE_MASK 7 #define SDS_TYPE_BITS 3 //過去指向sdshdr的指標,並賦給sh #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T))); //獲取指向sdshdr結構體的指標 #define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T)))) //獲取sdshdr5的字串長度,即flag欄位中高5位儲存的值 #define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)

SDS_HDR --> | len | alloc | flag | buf(sds) |
unsigned char flags = sds[-1]

  • len :sds的長度,不包括末尾結束符
  • alloc :分配的sds長度,不包括結束符
  • flag :sds型別
  • buf :sds實際存放位置

SDS 遵循 C 字串以空字元結尾的慣例, 儲存空字元的 1 位元組空間不計算在 SDS 的 len 屬性裡面, 並且為空字元分配額外的 1 位元組空間,遵循空字元結尾這一慣例的好處是, SDS 可以直接重用一部分 C 字串函式庫裡面的函式,末尾空字元的分配在sdsnewlen函式裡(sh = s_malloc(hdrlen+initlen+1);),下面有完整函式程式碼

SDS與C字串的區別

  • 常數複雜度O(1)獲取字串長度
    獲取一個 C 字串的長度, 程式必須遍歷整個字串, 對遇到的每個字元進行計數, 直到遇到代表字串結尾的空字元為止, 這個操作的複雜度為 O(N)
    SDS 在 len 屬性中記錄了 SDS 本身的長度, 所以獲取一個 SDS 長度的複雜度僅為 O(1)

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

  • 減少修改字串時帶來的記憶體重分配次數
    C 字串並不記錄自身的長度, 所以對於一個包含了 N 個字元的 C 字串來說, 這個 C 字串的底層實現總是一個 N+1 個字元長的陣列(額外的一個字元空間用於儲存空字元),每次增長或者縮短一個 C 字串, 程式都總要對儲存這個 C 字串的陣列進行一次記憶體重分配操作,通過未使用空間(alloc - len), SDS 實現了空間預分配惰性空間釋放兩種優化策略

    • 空間預分配
      如果對 SDS 進行修改之後, SDS 的長度(也即是 len 屬性的值)將小於 1 MB , 那麼程式分配和 len 屬性同樣大小的未使用空間, 這時 SDS alloc 屬性的值將是和len 屬性的2倍。 舉個例子, 如果進行修改之後, 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 。

      通過這種預分配策略, SDS 將連續增長 N 次字串所需的記憶體重分配次數從必定 N 次降低為最多 N 次。

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

      通過惰性空間釋放策略, SDS 避免了縮短字串時所需的記憶體重分配操作, 併為將來可能有的增長操作提供了優化

  • 二進位制安全
    C 字串中的字元必須符合某種編碼(比如 ASCII), 並且除了字串的末尾之外, 字串裡面不能包含空字元, 否則最先被程式讀入的空字元將被誤認為是字串結尾 —— 這些限制使得 C 字串只能儲存文字資料, 而不能儲存像圖片、音訊、視訊、壓縮檔案這樣的二進位制資料

    使用 SDS 來儲存之前提到的特殊資料格式就沒有任何問題, 因為 SDS 使用 len 屬性的值而不是空字元來判斷字串是否結束,通過使用二進位制安全的 SDS , 而不是 C 字串, 使得 Redis 不僅可以儲存文字資料, 還可以儲存任意格式的二進位制資料

  • 相容部分 C 字串函式
    SDS 的 API 都是二進位制安全的, 但它們一樣遵循 C 字串以空字元結尾的慣例: 這些 API 總會將 SDS 儲存的資料的末尾設定為空字元, 並且總會在為 buf 陣列分配空間時多分配一個位元組來容納這個空字元, 這是為了讓那些儲存文字資料的 SDS 可以重用一部分

C 字串 SDS
獲取字串長度的複雜度為 O(N) 獲取字串長度的複雜度為 O(1)
API 是不安全的,可能會造成緩衝區溢位 API 是安全的,不會造成緩衝區溢位
修改字串長度 N 次必然需要執行 N 次記憶體重分配 修改字串長度 N 次最多需要執行 N 次記憶體重分配
只能儲存文字資料 可以儲存文字或者二進位制資料
可以使用所有 <string.h> 庫中的函式 可以使用一部分 <string.h> 庫中的函式

SDS API

//<sds.h>
//獲取sds長度
static inline size_t sdslen(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)->len;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->len;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->len;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->len;
    }
    return 0;
}

//獲取sds可用的位元組數   
/* sdsalloc() = sdsavail() + sdslen() */
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字串的核心函式:

/* Create a new sds string with the content specified by the 'init' pointer
 * and 'initlen'.
 * If NULL is used for 'init' the string is initialized with zero bytes.
 * If SDS_NOINIT is used, the buffer is left uninitialized;
 *
 * The string is always null-termined (all the sds strings are, always) so
 * even if you create an sds string with:
 *
 * mystring = sdsnewlen("abc",3);
 *
 * You can print the string with printf() as there is an implicit \0 at the
 * end of the string. However the string is binary safe and can contain
 * \0 characters in the middle, as the length is stored in the sds header. */
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. */

	//為結尾空字元多分配1位元組的空間
    sh = s_malloc(hdrlen+initlen+1);
    if (init==SDS_NOINIT)
        init = NULL;
    else 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;
}

擴容的核心函式:

//#define SDS_MAX_PREALLOC (1024*1024)

/* 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);
    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) //SDS_MAX_PREALLOC (1024*1024) = 1M
        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;
}

對給定字元或字串進行分割:

/* Split 's' with separator in 'sep'. An array
 * of sds strings is returned. *count will be set
 * by reference to the number of tokens returned.
 *
 * On out of memory, zero length string, zero length
 * separator, NULL is returned.
 *
 * Note that 'sep' is able to split a string using
 * a multi-character separator. For example
 * sdssplit("foo_-_bar","_-_"); will return two
 * elements "foo" and "bar".
 *
 * This version of the function is binary-safe but
 * requires length arguments. sdssplit() is just the
 * same function but for zero-terminated strings.
 */
sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count) {
    int elements = 0, slots = 5;
    long start = 0, j;
    sds *tokens;

    if (seplen < 1 || len < 0) return NULL;
	
	//tokens相當於是一個二維陣列,預設先分配五個指標記憶體,即可以存5個被分割的字串
    tokens = s_malloc(sizeof(sds)*slots);
    if (tokens == NULL) return NULL;

    if (len == 0) {
        *count = 0;
        return tokens;
    }
    for (j = 0; j < (len-(seplen-1)); j++) {
        /* make sure there is room for the next element and the final one */
        if (slots < elements+2) {
            sds *newtokens;

            slots *= 2;
            newtokens = s_realloc(tokens,sizeof(sds)*slots);
            if (newtokens == NULL) goto cleanup;
            tokens = newtokens;
        }
        /* search the separator */
        if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) {
            tokens[elements] = sdsnewlen(s+start,j-start);
            if (tokens[elements] == NULL) goto cleanup;
            elements++;
            start = j+seplen;
            j = j+seplen-1; /* skip the separator */
        }
    }
    /* Add the final element. We are sure there is room in the tokens array. */
    tokens[elements] = sdsnewlen(s+start,len-start);
    if (tokens[elements] == NULL) goto cleanup;
    elements++;
    *count = elements;
    return tokens;

cleanup:
    {
        int i;
        for (i = 0; i < elements; i++) sdsfree(tokens[i]);
        s_free(tokens);
        *count = 0;
        return NULL;
    }
}

返回指定範圍內的字串:

/* Turn the string into a smaller (or equal) string containing only the
 * substring specified by the 'start' and 'end' indexes.
 *
 * start and end can be negative, where -1 means the last character of the
 * string, -2 the penultimate character, and so forth.
 *
 * The interval is inclusive, so the start and end characters will be part
 * of the resulting string.
 *
 * The string is modified in-place.
 *
 * Example:
 *
 * s = sdsnew("Hello World");
 * sdsrange(s,1,-1); => "ello World"
 */
void sdsrange(sds s, ssize_t start, ssize_t end) {
    size_t newlen, len = sdslen(s);

    if (len == 0) return;
    if (start < 0) {
        start = len+start;
        if (start < 0) start = 0;
    }
    if (end < 0) {
        end = len+end;
        if (end < 0) end = 0;
    }
    newlen = (start > end) ? 0 : (end-start)+1;
    if (newlen != 0) {
        if (start >= (ssize_t)len) {
            newlen = 0;
        } else if (end >= (ssize_t)len) {
            end = len-1;
            newlen = (start > end) ? 0 : (end-start)+1;
        }
    } else {
        start = 0;
    }
    if (start && newlen) memmove(s, s+start, newlen);
    s[newlen] = 0;
    sdssetlen(s,newlen);
}