天天看點

sds.h

redis中的sds結構解析

0丶源碼基于redis 6.2.7

1丶什麼是sds

sds 即: simple dynamic string,簡單動态字元串      

2丶redis為什麼使用sds,不适用c語言的字元串呢?

1丶sds可以在O(1)的時間範圍中擷取字元串長度,c語言需要周遊
2丶sds擁有自動擴容機制.
3丶sds擁有惰性空間釋放機制,減少了記憶體配置設定次數.
4丶sds是二進制安全的.      

3丶從源碼探究

3.1 ,下載下傳源碼

​​Download | Redis​​

3.2. 檢視源碼

打開sds.h

/*
 *  sdshdr5從未被使用過,隻是通路flag辨別;
 *  flag 低三位 存儲類型;  高5位 存儲資料長度.  2^5=32.  因為buf最後以/0結尾. 故最大長度為31.
 * typedef unsigned char     uint8_t;
   typedef unsigned short    uint16_t;
   typedef unsigned int      uint32_t;
   typedef unsigned __int64     uint64_t;
 * */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /*flag 低三位 存儲類型;  高5位 存儲資料長度.  2^5=32.  因為buf最後以/0結尾. 故最大長度為31.*/
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 已使用的資料長度 */
    uint8_t alloc; /* 總長度  */
    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[];
};      

短字元串,長度小于32時,使用的資料結構sdshdr5,結構如下.

sds.h

相比redis3.0.0結構:

sds.h
struct sdshdr {
    //記錄buf數組中已使用位元組的數量
    //等于SDS所儲存字元串的長度
    unsigned int len;

    //記錄buf數組中未使用位元組的數量
    unsigned int free;

    //char數組,用于儲存字元串
    char buf[];
};      

3.3. sds的優化

因為曆史版本的sds,無論字元串多長,它header裡面的len,free字段,都需要占用8個位元組的大小,這如果儲存小字元串實在是太浪費了,

故在6.2.7中我們發現,他根據不同的字元串長度,使用不同的結構類型. 如果字元串長度在32位以内,則隻多了1個位元組的長度來存儲字元串長度資訊.

4丶優勢解讀

4.1, 為什麼是O(1)的時間複雜度擷取字元串長度?

根據,6.2.7源碼中看出,無論字元串長度多少,隻是增加了一個字段.

unsigned char flags;      

1位=8位元組. flags字段,它使用第三位儲存資料結構

如下圖擷取字元串長度的源碼:

#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
//這裡的sizeof 傳回實際的類型
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
//falgs字段向右移動三位,就是字元串長度
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)

/**擷取字元串長度*/
static inline size_t sdslen(const sds s) {
    //因為sds的指針指向buf數組,則buf數組往前一位,就是falgs字段.
    unsigned char flags = s[-1];
    //flags字段的低3位是資料結構,與7 &; 則得到實際的資料結構.    
    // 7   = 0 0 0 0 0 1  1  1
    //flags= X X X X X X  X  X 
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            //因為高5位是字元串長度,拿到高5位的值即可.
            return SDS_TYPE_5_LEN(flags);
            
            //餘下方式都一樣,拿到實際對應的資料結構,見源碼3.2. 然後拿出期中的len字段
        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;
}      

由上圖可見,因為字元串長度是用屬性存儲的,故直接擷取即可,不需要像c語言一樣循環得出.

4.2 sds是二進制安全的.

** 什麼是二進制安全?**

通俗地講,C語言中,用’0’表示字元串的結束,如果字元串本身就有’0’字元,字元串就會被截斷,即非二進制安全;若通過某種機制,保證讀寫字元串時不損害其内容,則是二進制安全。

SDS使用len屬性的值判斷字元串是否結束,不會受’\0’的影響。

4.3 sds的的自動擴容

首先看C語言中的字元串拼接

char * __cdecl strcat (char * dst, const char * src)
{
    char * cp = dst;
 
    while( *cp )
        cp++; /* 找到 dst 的結尾 */
 
    while( *cp++ = *src++ ) ; /* 無腦将 src 複制到 dst 中; 要是dst的長度不夠呢?就會被覆寫!!!!!!!! */ 
 
    return( dst ); /* 傳回 dst */
}      

在看redis中的擴容:

sds sdscatsds(sds s, const sds t) {
    return sdscatlen(s, t, sdslen(t));
}

/* s: 源字元串
 * t: 待拼接字元串
 * len: 待拼接字元串長度
 */
sds sdscatlen(sds s, const void *t, size_t len) {
    // 擷取目前字元串的長度
    size_t curlen = sdslen(s);
  // SDS 自動擴容. 傳入原字元串長度與拼接字元串長度
    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
    // 将目标字元串拷貝至源字元串末尾
    memcpy(s+curlen, t, len);
    // 更新 SDS 長度
    sdssetlen(s, curlen+len);
    // 追加結束符
    s[curlen+len] = '\0';
    return s;
}


/* s: 源字元串
 * addlen: 新增長度
 */
sds sdsMakeRoomFor(sds s, size_t addlen) {
    void *sh, *newsh;
    // sdsavail: s->alloc - s->len, 擷取 SDS 的剩餘長度; 
    //如果是SDS_TYPE_5 則直接傳回0;
    size_t avail = sdsavail(s);
    size_t len, newlen, reqlen;
    // 根據 flags 擷取 SDS 的類型 oldtype
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    int hdrlen;
    size_t usable;

    /* Return ASAP if there is enough space left. */
    // 剩餘空間大于等于新增空間,無需擴容,直接傳回源字元串
    if (avail >= addlen) return s;
    // 擷取目前長度
    len = sdslen(s);
    // 
    sh = (char*)s-sdsHdrSize(oldtype);
    // 新長度
    reqlen = newlen = (len+addlen);
    // 斷言新長度比原長度長,否則終止執行
    assert(newlen > len);   /* 防止資料溢出 */
    // SDS_MAX_PREALLOC = 1024*1024, 即1MB
    if (newlen < SDS_MAX_PREALLOC)
        // 新增後長度小于 1MB ,則按新長度的兩倍擴容
        newlen *= 2;
    else
        // 新增後長度大于 1MB ,則按新長度加上 1MB 擴容
        newlen += SDS_MAX_PREALLOC;
    // 重新計算 SDS 的類型
    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. */
    // 不使用 sdshdr5 . 因為sdshdr5是沒有len屬性與alloc屬性 無法記住 空的 空間.
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;
    // 擷取新的 header 大小
    hdrlen = sdsHdrSize(type);
    assert(hdrlen + newlen + 1 > reqlen);  /* Catch size_t overflow */
    if (oldtype==type) {
        // 類型沒變
        // 調用 s_realloc_usable 重新配置設定可用記憶體,傳回新 SDS 的頭部指針
        // usable 會被設定為目前配置設定的大小
        newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL; // 配置設定失敗直接傳回NULL
        // 擷取指向 buf 的指針
        s = (char*)newsh+hdrlen;
    } else {
        // 類型變化導緻 header 的大小也變化,需要向前移動字元串,不能使用 realloc
        newsh = s_malloc_usable(hdrlen+newlen+1, &usable);
        if (newsh == NULL) return NULL;
        // 将原字元串copy至新空間中
        memcpy((char*)newsh+hdrlen, s, len+1);
        // 釋放原字元串記憶體
        s_free(sh);
        s = (char*)newsh+hdrlen;
        // 更新 SDS 類型
        s[-1] = type;
        // 設定長度
        sdssetlen(s, len);
    }
    // 擷取 buf 總長度(待定)
    usable = usable-hdrlen-1;
    if (usable > sdsTypeMaxSize(type))
        // 若可用空間大于目前類型支援的最大長度則截斷
        usable = sdsTypeMaxSize(type);
    // 設定 buf 總長度
    sdssetalloc(s, usable);
    return s;
}      

自動擴容機制總結:

擴容階段:

  • 若 SDS 中剩餘空閑空間 avail 大于新增内容的長度 addlen,則無需擴容; 注意:若是sdshdr5 則直接範圍0,必然擴容.
  • 若 SDS 中剩餘空閑空間 avail 小于或等于新增内容的長度 addlen:
  • 若新增後總長度 len+addlen < 1MB,則按新長度的兩倍擴容;
  • 若新增後總長度 len+addlen > 1MB,則按新長度加上 1MB 擴容。

記憶體配置設定階段:

  • 根據擴容後的長度選擇對應的 SDS 類型:
  • 若類型不變,則隻需通過​

    ​s_realloc_usable​

    ​擴大 buf 數組即可;
  • 若類型變化,則需要為整個 SDS 重新配置設定記憶體,并将原來的 SDS 内容拷貝至新位置。

4.4 惰性空間釋放機制,空間預配置設定政策

1丶空間預配置設定政策. 這樣子擴容N次,SDS 将連續增長N次字元串所需的記憶體重配置設定次數從必定N次降低為最多N次。

見4.3的擴容邏輯,因為每次是*2或者增加1M大小,預留了空間,減少記憶體配置設定的次數.

2丶惰性空間釋放機制

空間預配置設定,是減少增加字元串擴容的次數,凍醒空間釋放則相反,是優化了減少字元串空間配置設定的次數.

sds sdstrim(sds s, const char *cset) {
    char *start, *end, *sp, *ep;
    size_t len;

    sp = start = s;
    ep = end = s+sdslen(s)-1;
    // strchr()函數用于查找給定字元串中某一個特定字元
    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';
    // 僅僅更新了len
    sdssetlen(s,len);
    return s;
}      
sds sdsRemoveFreeSpace(sds s) {
    void *sh, *newsh;
    //sds類型
    char type, oldtype = s[-1] & SDS_TYPE_MASK;
    // 擷取 header 大小
    int hdrlen, oldhdrlen = sdsHdrSize(oldtype);
    //原字元串長度
    size_t len = sdslen(s);
    //原sds剩餘空間
    size_t avail = sdsavail(s);
    //擷取指向頭部的指針; s的指針
    sh = (char*)s-oldhdrlen;

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

    /* 擷取實際需要的類型,與header大小 */
    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);
    }
    sdssetalloc(s, len);
    return s;
}      
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);
 }
 sdssetalloc(s, len);
 return s;
 }