Redis 設計與實現 3:字符串 SDS

本文的分析沒有特殊說明都是基於 Redis 6.0 版本源碼
redis 6.0 源碼:https://github.com/redis/redis/tree/6.0git

在 Redis 中,字符串都用自定義的結構簡單動態字符串(Simple Dynamic Strings,SDS)
Redis 中使用到的字符串都是用 SDS,例如 key、string 類型的值、sorted set 的 member、hash 的 field 等等等等。。。github

數據結構

舊版本的結構

3.2 版本以前,sds 的定義是這樣的:redis

struct sdshdr {
	// buf 數組中已使用的字節數量,也就是 sds 自己的字符串長度
    unsigned int len;
    // buf 數組中未使用的字節數量
    unsigned int free;
    // 字節數組,用於保存字符串
    char buf[];
};

舊版本 SDS 結構示例

這樣的結構有幾個好處數組

  • 單獨記錄長度len,獲取字符串長度的時間複雜度是 $O(1)$ 。傳統的 C 字符串獲取長度須要遍歷字符串,直到遇到\0,時間複雜度是 $O(N)$。
  • buf 數組末尾遵循 C 字符串以 \0 結尾的慣例,能夠兼容 C 處理字符串的函數。
  • 減小修改字符串帶來的內存重分配次數,Redis 使用了 空間預分配(預先申請大一點點的空間) 和 空間惰性釋放(字符串變短修改len字段便可)來減小字符串修改引發的內存從新分配。
  • 不以\0爲結尾的判斷,二進制安全。由於圖片等二進制數據中,可能包含\0,傳統 C 字符串一遇到 \0 就認爲字符串結束了,會致使不能完整保存。

缺點:安全

  • lenfree 的定義用了 4 個字節,能夠表示 2^32 的長度。可是咱們實際使用的字符串,每每沒有那麼長。4 個字節形成了浪費。

新版本的結構

舊版本中咱們說到,lenfree 的缺點是用了太長的變量,新版本解決了這個問題。
咱們來看一下新版本的 SDS 結構。數據結構

在 Redis 3.2 版本以後,Redis 將 SDS 劃分爲 5 種類型:函數

類型 字節
sdshdr5 < 1 <8
sdshdr8 1 8
sdshdr16 2 16
sdshdr32 4 32
sdshdr64 8 64

新版本新增長了一個 flags 字段來標識類型,長度 1 字節(8 位)。
類型只佔用了前 3 位。在 sdshdr5 中,後 5 位用來保存字符串的長度。其餘類型後 5 位沒有用。優化

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 前 3 位保存類型,後 5 位保存字符串長度 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 字符串長度,1 字節 8 位 */
    uint8_t alloc; /* 申請的總長度,1 字節 8 位 */
    unsigned char flags; /* 前 3 位保存類型,後 5 位未使用 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* 字符串長度,2 字節 16 位 */
    uint16_t alloc; /* 申請的總長度,2 字節 16 位 */
    unsigned char flags; /* 前 3 位保存類型,後 5 位未使用 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* 字符串長度,4 字節 32 位 */
    uint32_t alloc; /* 申請的總長度,4 字節 32 位 */
    unsigned char flags; /* 前 3 位保存類型,後 5 位未使用 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* 字符串長度,8 字節 64 位 */
    uint64_t alloc; /* 申請的總長度,8 字節 64 位 */
    unsigned char flags; /* 前 3 位保存類型,後 5 位未使用 */
    char buf[];
};

優勢:ui

  • 舊版本相對於傳統 C 字符串的優勢,新版本都有
  • 相對於舊版本,新版本能夠經過字符串的長度,選擇不一樣的結構,能夠節約內存
  • 使用 __attribute__ ((__packed__)) ,讓編譯器取消結構在編譯過程當中的優化對齊,按照實際佔用字節數進行對齊,能夠節約內存

SDS 的初始化

sds 的定義,跟傳統的C語言字符串保持類型兼容 char *。可是 sds 是二進制安全的,中間可能包含\0指針

sds.h

typedef char *sds;

sds.c

// 初始化 sds
sds sdsnewlen(const void *init, size_t initlen) {
	// 指向 sdshdr 開始地方的指針
    void *sh;
    // sds 實際是一個指針,指向 buf 開始的位置
    sds s;
    // 根據初始化的長度,返回 sds 的類型
    char type = sdsReqType(initlen);
    // initlen == 0,是空字符串,空字符串每每就是用來日後添加字節的,使用 SDS_TYPE_8 比 SDS_TYPE_5 更好
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    // 根據類型獲取 struct sdshdr 的長度
    int hdrlen = sdsHdrSize(type);
    // flags 字段的指針
    unsigned char *fp;
	
	// 開始分配空間,+1 是爲了最後一個的結束符號 \0
    sh = s_malloc(hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    // const char *SDS_NOINIT = "SDS_NOINIT";
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
    	// 不是 init 則清空 sh 的內存
        memset(sh, 0, hdrlen+initlen+1);
    // s 指向了 buf 開始的地址
    // 從上面結構能夠看出,內存地址的順序: len, alloc, flag, buf
    // 由於 buf 自己不佔用空間,hdrlen 實際上就是結構的頭(len、alloc、flags)
    s = (char*)sh+hdrlen;
    // flags 佔用 1 個字節,因此 s 退一位就是 flags 的開始位置了
    fp = ((unsigned char*)s)-1;
    switch(type) {
        case SDS_TYPE_5: {
        	// #define SDS_TYPE_BITS 3
        	// 前 3 位保存類型,後 5 位保存長度
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
        	// define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
        	// sh 變量賦值了 struct sdshdr
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
        // 下面是對 SDS_TYPE_1六、SDS_TYPE_3二、SDS_TYPE_64 的初始化,跟 SDS_TYPE_8 的相似,篇幅有限,省略...
    }
    // 若是 init 非空,則把 init 字符串賦值給 s,實際上也是 buf 的初始化
    if (initlen && init)
        memcpy(s, init, initlen);
   	// 最後加一個結束標誌 \0
    s[initlen] = '\0';
    return s;
}

SDS 的擴/縮容

擴容

擴容就不跟初始化同樣寫註釋寫得那麼詳細了,直接拉最重要的幾句代碼就行。

sds sdsMakeRoomFor(sds s, size_t addlen) {
    // #define SDS_MAX_PREALLOC (1024*1024)
    // 當新的長度小於 1M 的時候,長度會增加一倍
    // 當新的長度達到 1M 以後,最多就增加 1M 了
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
    // ...
}

縮容

sds 縮短不會真正縮小 buf,而是隻改長度而已,類型也不變。

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

sds.h

static inline void sdssetlen(sds s, size_t newlen) {
	// 設置sds長度,只是修改 sdshdr 結構中的長度字段,類型不會變
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            {
                unsigned char *fp = ((unsigned char*)s)-1;
                *fp = (unsigned char)(SDS_TYPE_5 | (newlen << SDS_TYPE_BITS));
            }
            break;
        case SDS_TYPE_8:
            SDS_HDR(8,s)->len = (uint8_t)newlen;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->len = (uint16_t)newlen;
            break;
        case SDS_TYPE_32:
            SDS_HDR(32,s)->len = (uint32_t)newlen;
            break;
        case SDS_TYPE_64:
            SDS_HDR(64,s)->len = (uint64_t)newlen;
            break;
    }
}
相關文章
相關標籤/搜索