redis原始碼分析(二)、sds動態字串學習總結

後開啟撒打發了發表於2017-12-20

sds字串

Redis 只會使用 C 字串作為字面量, 在大多數情況下, Redis 使用 SDS (Simple Dynamic String,簡單動態字串)作為字串表示。 比起 C 字串, SDS 具有以下優點:

  1. 常數複雜度獲取字串長度。
  2. 杜絕緩衝區溢位。
  3. 減少修改字串長度時所需的記憶體重分配次數。
  4. 二進位制安全。
  5. 相容部分 C 字串函式。

根據傳統, C 語言使用長度為N+1的字元陣列來表示長度為 N 的字串,並且字元陣列的最後一個元素總是空字元 '\0' 。

C 語言使用的這種簡單的字串表示方式, 並不能滿足 Redis 對字串在安全性、效率、以及功能方面的要求, 本節接下來的內容將詳細對比 C 字串和 SDS 之間的區別, 並說明 SDS 比 C 字串更適用於 Redis 的原因。

SDS又叫簡單動態字串,在Redis中預設使用SDS來表示字串。比如在Redis中的鍵值對中的鍵一般都是使用SDS來實現。首先需要說明的是在Redis中,字串不是用傳統的字串來實現,而是Redis自己構建了一個結構來表示字串。優點如下:

1、O(1)時間內獲取字串長度。常數複雜度獲取字串長度(依據其結構特性,只需要訪問其結構體成員len既可獲得字串長度)

2、杜絕緩衝區溢位,另外SDS還提供的一些API操作,是二進位制安全的(也就是不會因為空格等特殊字元而中斷字串)、不會溢位(API操作會檢查其長度)

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

  • 對於增長字串其採用的策略是檢查修改之後的長度大小,如果小於1024*1024,則分配2倍的修改後的長度+1
  • 對於減少的字串其並不立即釋放空間,而是迴歸到alloc中去。(自動查閱redis記憶體分配與釋放策略)
typedef char *sds;//底層用於儲存字串的資料結構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 {
    //低三位儲存type,高5位儲存長度
    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表示的字串是有SDS header和char*指標組成,而SDS的頭部主要由四部分組成:

  • len:SDS字串已使用的空間。
  • alloc:申請的空間,減去len就是未使用的空間,初始時和len一致。
  • flag:只使用了低三位表示型別,細化了SDS的分類,根據字串的長度的不同選擇不同的sds結構體,而結構體的主要區別是len和alloc的型別,這樣做可以節省一部分空間大小,畢竟在redis字串非常多,進一步的可以節省空間。
  • buf: 用了C的特性表示不定長字串。

除了sdshdr5之外,其他結構都是相似的。我們先看看sdshdr,它只有flags和buf成員,其中flag空間被C充分利用,其第三位儲存了SDS字串的型別.

#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的長度,低三位儲存了type
/*sds結構一共有五種Header定義,其目的是為了滿足不同長度的字串可以使用不同大小的Header,從而節省記憶體。 
Header部分主要包含以下幾個部分:len、alloc、flags其中
len:表示字串真正的長度,不包含空終止字和alloc以及flag
alloc:表示字串的最大容量,不包含Header和最後的空終止字元和flag
flag:只用了3位表示sds的type 就是表示header的型別*/
// 五種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

sdsnewlen函式

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.
 *
 * 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;

    // 根據initlen 求sds使用的結構體型別
    char type = sdsReqType(initlen);

    //下面是我在測試的時候新增的列印字串型別程式碼,原始碼中沒有
    int type_t = type;
    printf("sds.c------->sdsnewlen: the sds type is: [ %d ]\n", type_t);
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */

    //type5 不再使用 而是直接使用type8
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;

    //sds使用的結構體的大小
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */  //flag的第三位表示sds使用的結構體

    //分配內層 大小是sds結構體的大小: hdrlen + buf的大小initlen + '\0' 
    sh = s_malloc(hdrlen+initlen+1);
    //init為null的時候  直接初始化sh內容
    if (!init)
        memset(sh, 0, hdrlen+initlen+1); 
    //sh 分配失敗的情況直接返回null
    if (sh == NULL) return NULL;

    // s為資料部分的起始指標 指向buf地址指標
    s = (char*)sh+hdrlen;
    //fp指向flag  flag的低三位表示sds使用的結構體型別
    fp = ((unsigned char*)s)-1;

    //根據sds使用的結構體型別 給結構體中的成員賦值
    switch(type) {
        case SDS_TYPE_5: {
            // initlen << SDS_TYPE_BITS 把initlen的值儲存到flag的高5位中去  低三位儲存type的值
            *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);  

    // 與C字串相容
    s[initlen] = '\0';  

    // 返回建立的sds字串指標
    return s;
}

sdsMakeRoomFor函式

函式原型:sds sdsMakeRoomFor(sds s, size_t addlen)

  • 說明:實現擴充已有sds的可用空間為指定的大小,擴充規則是:當addlen的長度小於1024*1024時,則申請的空間是2*(addlen+len),否則擴充為1024*1024大小。
  • 返回值:擴充後的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); //返回剩餘可用的空間。
    size_t len, newlen;
    char type, oldtype = s[-1] & SDS_TYPE_MASK; //獲取type
    int hdrlen;

    /* Return ASAP if there is enough space left. */
    if (avail >= addlen) return s; //如果可用空間大於addlen直接返回舊的字串

    len = sdslen(s); //求sds的長度
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    
    //根據newlen調整sds的type
    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;
    
    //根據type獲取sds使用的結構體的長度
    hdrlen = sdsHdrSize(type); 
  
   //若型別和原有型別一樣,則採用realloc分配空間,否則重新分配採用malloc函式分配空間
   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中的字串拷貝到newsh指向的buf中去
        s_free(sh);//釋放舊的sh空間
        s = (char*)newsh+hdrlen; //s指向重新分配的字串
        s[-1] = type; //指定newsds的type
        sdssetlen(s, len); //設定新的字串的長度
    }
    sdssetalloc(s, newlen); //設定新的newsh的分配的空間長度
    return s;
}

**sdscatlen **:sds提供了字串的連線函式,用來連線兩個字串

//sds字串連線
sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);

    //擴充套件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 sdsempty(void); // 清空sds
sds sdsdup(const sds s); // 複製字串
sds sdsgrowzero(sds s, size_t len); // 擴充套件字串到指定長度
sds sdscpylen(sds s, const char *t, size_t len); // 字串的複製
sds sdscpy(sds s, const char *t); // 字串的複製
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);   //更新字串最新的長度
void sdsclear(sds s);   //字串清空操作
void sdstolower(sds s);    //sds字元轉小寫表示
void sdstoupper(sds s);    //sds字元統一轉大寫
sds sdsjoin(char **argv, int argc, char *sep);   //以分隔符連線字串子陣列構成新的字串

sdsull2str 把一個long long的型別的數轉成字串

#include <iostream>
using namespace std;  
int sdsll2str(char *s, long long value) {
    char *p, aux;
    unsigned long long v;
    size_t l;

    /* Generate the string representation, this method produces
     * an reversed string. */
    v = (value < 0) ? -value : value;
    p = s;
    do {
        *p++ = '0'+(v%10);
        v /= 10;
    } while(v);
    if (value < 0) *p++ = '-';

    /* Compute length and add null term. */
    l = p-s;
    *p = '\0';

    /* Reverse the string. */
    p--;
    while(s < p) {
        aux = *s;
        *s = *p;
        *p = aux;
        s++;
        p--;
    }

//	std::cout << s << std::endl;
    return l;
}

  
int main()  
{  
	
	char str[10]={0};
	sdsll2str(str, 1234567);
	std::cout << str << std::endl;
}

相關文章