Redis源碼閱讀【1-簡單動態字符串】

Redis源碼閱讀【1-簡單動態字符串】
Redis源碼閱讀【2-跳躍表】
Redis源碼閱讀【3-Redis編譯與GDB調試】
Redis源碼閱讀【4-壓縮列表】
Redis源碼閱讀【5-字典】
Redis源碼閱讀【6-整數集合】
Redis源碼閱讀【7-quicklist】
Redis源碼閱讀【8-命令處理生命週期-1】
Redis源碼閱讀【8-命令處理生命週期-2】
Redis源碼閱讀【8-命令處理生命週期-3】
Redis源碼閱讀【8-命令處理生命週期-4】
Redis源碼閱讀【番外篇-Redis的多線程】
建議搭配源碼閱讀源碼地址

1、介紹

簡單動態字符串(Simple Dynamic Strings SDS)是Redis的基本數據結構之一,主要用於存儲字符串和整型數據。SDS兼容C語音標準字符串處理函數,並且在此保證了二進制安全

二進制安全主要是針對類似於 \0 等有特殊含義的轉義字符保證其安全性,而且不損害其內容

2、SDS 基本結構

首先我們看看SDS在C語言中的基本結構體是怎麼樣的

struct sds {
	int len;  // buf 已經佔用的字節長度
	int alloc; // 總長度 (不包括頭和空終止符)
	char buf[]; // 數據空間
}
//這裏的成員屬性不一定就是使用 int 也可能是更大或者更小的數據類型

SDS基本結構如下圖所示(屬性長度不一定就是4):
SDS基本結構
之所以使用這種方式來存放字符串,是因爲SDS結構體中的地址是連續的,這樣能通過偏移量的方式快速查找內存內容,同時也能通過buf的地址非常快速獲取結構體SDS的首地址。

3、SDS 類型

從上面的圖片中看出,SDS 佔用的空間,除了本身buf 實際數據佔用的空間,還有 len alloc 等結構屬性也會佔用一定的空間大小,但是如果Redis中存儲了大量的短字符,那麼這種結構體的頭部無疑是對空間的浪費,而Redis本身就是主打性能和空間,這樣的空間浪費,是不能容忍的,於是Redis 對 SDS 做出了不同的劃分,分別如下:

//小於一字節
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; 
    char buf[];
};

//一字節
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;
    uint8_t alloc; 
    unsigned char flags; 
    char buf[];
};

//2字節
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; 
    uint16_t alloc; 
    unsigned char flags; 
    char buf[];
};

//4字節
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; 
    uint32_t alloc; 
    unsigned char flags; 
    char buf[];
};

//8字節
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; 
    uint64_t alloc; 
    unsigned char flags; 
    char buf[];
};
//注意這樣都是使用uint 這樣能最大的使用空間

如上述代碼中展示的那樣,在結構體中引入了一個 flags 的單字節標記來區分SDS的類型

在單字節模式下 flags 的空間使用如下圖所示:
flage結構
其中,低3位用來區分當前SDS的類型,高5位用來存儲當前SDS的數據長度,所以 SDS5 的範圍也只有【0~31】,flags 後面也就是實際的數據內容了,除此之外其它的 SDS 類型的頭結構基本上就是 len ,alloc ,flags 所以頭部空間基本上就是 S[len + alloc + flags],其中 len 和 alloc 根據不同類型的SDS使用不同的大小,以保證節約空間的目的,同時 flags 與 SDS5 一樣,只有前三位存儲類型,而後五位不存儲數據

 __attribute__ ((__packed__))

需要關注這塊,結構體會按照其所有變量結構體做最小公倍數字節對齊。當使用 packed 修飾後,結構體會按照 1字節對齊。以 SDS32 爲例 ,修飾前按照 12(4x3)字節對齊,修飾後按照1字節對齊。
修飾前後內存空間如下圖所示。
結構體對齊
這樣做有一下幾個好處:

1、節約內存:如SDS32可以節省3個字節
2、buf指針引用:SDS返回給上層的,不是結構體首地址,而是 buf 指針地址,這樣可以通過 buf[-1] 直接獲得 flags ,來識別當前 sds 結構體的類型,從而獲取整個結構體的任意一個部分

4、 創建字符串

Redis 通過 sdsnewlen 函數創建 SDS。函數會根據字符串長度來選擇合適的SDS 類型,待數據填入完成後,會返回 SDS buf 的指針作爲 SDS 的指針。
如下代碼:

/**
 * 入參有兩個,一個是初始化字符串的指針,另一個是當前字符串的字節長度
 */
sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    //根據長度選擇合適的sds類型
    char type = sdsReqType(initlen);
    //如果本身是空字符,那麼直接使用SDS8 而不是 SDS5 因爲 SDS5 不適合空字符
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
    //計算當前類型SDS 頭部字節大小
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags 指針. */
    // 按照 頭部空間 + 字符串大小 + 1 分配空間 (+1是爲了結束符號 \0)
    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; // s 是指向 buf 的指針
    fp = ((unsigned char*)s)-1; //s 是buf的指針 -1 即指向 flags
    //按照類型初始化 SDS
    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;
}

5、釋放字符串

SDS提供了直接釋放內存的方法-sdsfree,該方法通過對 sds 指針的偏移,可以定位到 sds 的首部,然後調用 s_free釋放內存:

void sdsfree(sds s) {
    if (s == NULL) return;
    s_free((char*)s-sdsHdrSize(s[-1])); //這裏的s_free 就是 free
}

除此之外 sds 還提供了 sdsclear 方法去清空字符串,目的是爲了優化性能,不直接釋放內存,而是將sds的len設置爲0,新的數據可以在此之上覆蓋,從而不必再重新分配內存。

void sdsclear(sds s) {
    sdssetlen(s, 0); //設置 len 爲0
    s[0] = '\0';  // buf 直接設置爲結束字符
}

sdsclear 和 sdsfree 的差別是 sdsfree 會直接調用 free 是直接釋放內存的使用權,而 sdsclear只是清空,允許後續相近的字符串能在此之上進行使用,場景並不是很通用,但是性能上比 sdsfree 要好

6、拼接字符串

拼接字符是通過sdscatsds來實現的

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

sdscatsds 是封裝給上層使用的,sdscatlen纔是具體的實現。調用sdscatlen可能會發生擴容的場景,其中調用sdsMakeRoomFor去檢查字符串是否需要擴容,若無需擴容則直接返回,需要擴容會返回擴容後的 sds。
代碼如下:

/**
 * 兩個入參,原sds  和 需要增加的空間大小
 */
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;
    int hdrlen;
    //有足夠的空間不需要擴容
    if (avail >= addlen) return s;
    
    len = sdslen(s); //獲取當前sds 的長度
   
    sh = (char *) s - sdsHdrSize(oldtype); //定位到 sds 的頭部 【buf地址 - 當前header長度】
    
    newlen = (len + addlen);  //獲得目標需要的總共大小空間
    
    if (newlen < SDS_MAX_PREALLOC) //新長度大於 1mb 的按照 2倍擴容  SDS_MAX_PREALLOC 是最小分配大小
        newlen *= 2;
    else //新長度小於 1mb 的 按照 1mb 擴容 SDS_MAX_PREALLOC 是最小分配大小
        newlen += SDS_MAX_PREALLOC;
    type = sdsReqType(newlen); //按照新大小確定需要分配的sds類型
   
    //強制把 sds5 變成 sds8  因爲 sds 5 是無法得知剩餘空間的 不支持擴容
    if (type == SDS_TYPE_5) type = SDS_TYPE_8;
   
    hdrlen = sdsHdrSize(type);//根據新的sds類型確定 頭部長度
   
    //判斷新舊類型是否一樣
    if (oldtype == type) {
        newsh = s_realloc(sh, hdrlen + newlen + 1); //追加分配分配sds 的 buf 空間  realloc 擴大空間
        if (newsh == NULL) return NULL;
        s = (char *) newsh + hdrlen; //定位到新sds 的 buf 位置
    } else {
        newsh = s_malloc(hdrlen + newlen + 1); //分配新的sds空間
        if (newsh == NULL) return NULL;
        memcpy((char *) newsh + hdrlen, s, len + 1);
        s_free(sh);  //釋放舊的空間
        s = (char *) newsh + hdrlen; //定位到 buf 位置
        s[-1] = type;
        sdssetlen(s, len); //初始化 len
    }
    
    sdssetalloc(s, newlen); //初始化 alloc
    return s;
}

7、其餘的API

函數名 說明
sdsempty 創建一個空字符,長度爲0 內容爲""
sdsnew 根據給定的C字符串創建sds
sdsdup 複製給定的sds
sdsupdatelen 手動刷新sds相關統計值
sdsRemoveFreeSpace 縮容處理,與擴容相反
sdsAllocSize 返回給定sds當前佔用內存大小
sdsgrowzero 將sds擴容到指定長度,並用0填充新增加內容
sdscpylen 將C字符複製到給定sds中
sdstrim 從sds兩端清除所有給定字符
sdscmp 比較兩個給定sds的實際大小
sdssplitlen 按照給定的分隔符號對sds切分
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章