Redis5.x底層數據結構之——字符串

1 簡單動態字符串

Redis是用C語言寫的,但是Redis的字符串不是 C 語言中的字符串(即以空字符’\0’結尾的字符數組)。Redis自己定義了一種名為簡單動態字符串(simple dynamic string,SDS)的抽象類型,并將SDS作為Redis的默認字符串表示。
SDS的定義在sds.h中:

/* 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[];
};

SDS由4部分組成:
len:SDS字符串已經使用的空間(不包含C中字符串的結束符'\0'的長度1)。
alloc:申請的空間大小,減去len就是未使用的空間,初始時和len相等。
flags:使用低三位表示類型,細分SDS的分類。方便根據字符串的長度不同選擇不用的SDS結構體,節省一部分空間。
buf:使用C的不定長字符串。

從SDS的定義上看,5種定義對應了最大長度不同的字符串。定義這5種不同的類型是為了盡量減少sdshdr占用的空間。
為了區分sdshdr屬于哪一種類型,在每一種定義中都加上了一個8bit的flags字段,用其中的低3位標識sdshdr的類型。
在C/C++中,建立一個結構體時,會進行字節對齊操作,使得結構體的大小比其變量占用的字節要多一些。為了能從buf直接找到到flags,定義時在結構體聲明中加上__attribute__((__packed__)), 強制不要按字節對齊(表示取消字節對齊,按照緊湊排列的方式),這樣不管是哪種類型的hdr,都可以用buf[-1]找到對應的flags。

2 SDS與C字符串的區別

C語言使用長度為N+1的字符串數組來表示長度為N的字符串;字符串數組的最后一個元素一定是'\0'。C語言的這種表示字符串的方式,并不能滿足Redis對字符串在安全性、功能性以及效率性的要求。
使用SDS除了用C語言中的字符串數組buf[]表示了字符串的內容,還記錄了Redis為該字符串分配的buf空間的總長度alloc、buf[]中已經使用的長度len。Redis使用SDS有以下幾個好處。

2.1 常數復雜度獲取字符串長度

由于存在len屬性,獲取SDS字符串的長度只需要讀取len屬性,時間復雜度為O(1)。而對于 C 語言,獲取字符串的長度通常是經過遍歷計數來實現的,時間復雜度為O(n)。通過strlen key命令獲取key的字符串長度的時間復雜度為O(1),可以反復執行而不會出現性能瓶頸。

2.2 杜絕緩沖區溢出

在 C 語言中使用strcat函數來進行兩個字符串的拼接,一旦沒有分配足夠長度的內存空間,就會造成緩沖區溢出。而對于 SDS 數據類型,在進行字符修改的時候,會首先根據記錄的len屬性檢查內存空間是否滿足需求,如果不滿足,會進行相應的空間擴展,然后在進行修改操作,所以不會出現緩沖區溢出。

2.3 減少修改字符串的內存重新分配次數

C語言由于不記錄字符串的長度,所以如果要修改字符串,必須要重新分配內存(先釋放再申請),因為如果沒有重新分配,字符串長度增大時會造成內存緩沖區溢出,字符串長度減小時會造成內存泄露。
  而對于SDS,由于len屬性和alloc屬性的存在,對于修改字符串SDS實現了空間預分配和惰性空間釋放兩種策略:

  1. 空間預分配
    對字符串進行空間擴展的時候,擴展的內存比實際需要的多,這樣可以減少連續執行字符串增長操作所需的內存重分配次數。
    額外分配空間策略:
  • 1.1. 如果SDS修改之后,SDS的長度(len屬性的值)將小于1MB(1024*1024 Byte),那么程序將分配和len屬性相同的大小的未使用空間,分配之后,alloc=len2。
    示例:
    SDS當前的長度len為6,alloc為12。此時執行append方法,在后面追加
    hello redis!*字符串,修改之后的長度len將變成18。修改之后,SDS的len為18,同時分配18Byte的未使用空間,alloc為36。最終,buf[]的實際長度將變成alloc+1Byte=36Byte+1Byte(1Byte用于保存空字符'\0')=37Byte。

  • 1.2 如果SDS進行修改之后,SDS的長度將大于等于1MB(1024*1024 Byte),那么程序會分配1MB的未使用空間。
    示例:
    SDS當前長度為0,len=0,alloc=0。此時執行set方法,字符串長度為2MB。那么執行之后,len=2*1024*1024,alloc=len+1024*1024=3*1024*1024,buf[]的實際長度為:2MB+1MB+1Byte。
    0sds.h中:

#define SDS_MAX_PREALLOC (1024*1024)

sds.c中

    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
  1. 惰性空間釋放
    對字符串進行縮短操作時,程序不立即使用內存重新分配來回收縮短后多余的字節,而是使用alloc屬性將所有字節的數量記錄下來,等待后續使用(當然SDS也提供了相應的API,當我們有需要時,也可以手動釋放這些未使用的空間)。

2.4 二進制安全

因為C字符串以空字符作為字符串結束的標識,而對于一些二進制文件(如圖片、音頻、視頻、壓縮文件等),內容可能包括空字符串,因此C字符串無法正確存取;而所有 SDS 的API 都是以處理二進制的方式來處理 buf 里面的元素,并且 SDS 不是以空字符串來判斷是否結束,而是以 len 屬性表示的長度來判斷字符串是否結束,因此SDS是二進制安全的。Redis的buf[]是字節數組,因為buf[]不是用于保存字符,而是保存一系列二進制數據。Redis使用二進制安全的SDS,可以保存任意格式的二進制數據。

2.5 兼容部分 C 字符串函數

雖然 SDS 是二進制安全的,但是一樣遵從每個字符串都是以空字符串結尾的慣例,這樣可以重用 C 語言庫<string.h> 中的一部分函數,避免了不必要的代碼重復。

綜上,用下面表格總結C字符串和SDS之間的區別。

C字符串 SDS
獲取字符串長度的復雜度為O(N) 獲取字符串長度的復雜度為O(1)
API是不安全的,可能會造成緩沖區溢出 API是安全的,不會造成緩沖區溢出
修改字符串長度N次需要執行N次的內存重分配 修改字符串長度N次最多需要執行N次的內存重分配
空字符'\0'作為文本數據的結束,只能保存文本數據 二進制安全,可以保存文本數據和所有格式的二進制數據
可以使用所有的C語言庫中的函數,如<string.h>/strcasecmp 可以使用部分C語言庫中的函數,如<string.h>/strcasecmp

3 SDS常用API

SDS常用API和常量定義。此處不詳細解釋,基本都可以根據函數名知道函數功能。
詳細參考Redis的sds.h源碼和sds.h的實現sds.c源碼。

#ifndef __SDS_H
#define __SDS_H

#define SDS_MAX_PREALLOC (1024*1024)
const char *SDS_NOINIT;

#include <sys/types.h>
#include <stdarg.h>
#include <stdint.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[];
};

#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
#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))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)

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

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

static inline void sdssetlen(sds s, size_t newlen) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            {
                unsigned char *fp = ((unsigned char*)s)-1;
                *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
            }
            break;
        case SDS_TYPE_8:
            SDS_HDR(8,s)->len = newlen;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->len = newlen;
            break;
        case SDS_TYPE_32:
            SDS_HDR(32,s)->len = newlen;
            break;
        case SDS_TYPE_64:
            SDS_HDR(64,s)->len = newlen;
            break;
    }
}

static inline void sdsinclen(sds s, size_t inc) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            {
                unsigned char *fp = ((unsigned char*)s)-1;
                unsigned char newlen = SDS_TYPE_5_LEN(flags)+inc;
                *fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
            }
            break;
        case SDS_TYPE_8:
            SDS_HDR(8,s)->len += inc;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->len += inc;
            break;
        case SDS_TYPE_32:
            SDS_HDR(32,s)->len += inc;
            break;
        case SDS_TYPE_64:
            SDS_HDR(64,s)->len += inc;
            break;
    }
}

/* sdsalloc() = sdsavail() + sdslen() */
static inline size_t sdsalloc(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)->alloc;
        case SDS_TYPE_16:
            return SDS_HDR(16,s)->alloc;
        case SDS_TYPE_32:
            return SDS_HDR(32,s)->alloc;
        case SDS_TYPE_64:
            return SDS_HDR(64,s)->alloc;
    }
    return 0;
}

static inline void sdssetalloc(sds s, size_t newlen) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5:
            /* Nothing to do, this type has no total allocation info. */
            break;
        case SDS_TYPE_8:
            SDS_HDR(8,s)->alloc = newlen;
            break;
        case SDS_TYPE_16:
            SDS_HDR(16,s)->alloc = newlen;
            break;
        case SDS_TYPE_32:
            SDS_HDR(32,s)->alloc = newlen;
            break;
        case SDS_TYPE_64:
            SDS_HDR(64,s)->alloc = newlen;
            break;
    }
}

sds sdsnewlen(const void *init, size_t initlen);
sds sdsnew(const char *init);
sds sdsempty(void);
sds sdsdup(const sds s);
void sdsfree(sds s);
sds sdsgrowzero(sds s, size_t len);
sds sdscatlen(sds s, const void *t, size_t len);
sds sdscat(sds s, const char *t);
sds sdscatsds(sds s, const sds t);
sds sdscpylen(sds s, const char *t, size_t len);
sds sdscpy(sds s, const char *t);

sds sdscatvprintf(sds s, const char *fmt, va_list ap);
#ifdef __GNUC__
sds sdscatprintf(sds s, const char *fmt, ...)
    __attribute__((format(printf, 2, 3)));
#else
sds sdscatprintf(sds s, const char *fmt, ...);
#endif

sds sdscatfmt(sds s, char const *fmt, ...);
sds sdstrim(sds s, const char *cset);
void sdsrange(sds s, ssize_t start, ssize_t end);
void sdsupdatelen(sds s);
void sdsclear(sds s);
int sdscmp(const sds s1, const sds s2);
sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count);
void sdsfreesplitres(sds *tokens, int count);
void sdstolower(sds s);
void sdstoupper(sds s);
sds sdsfromlonglong(long long value);
sds sdscatrepr(sds s, const char *p, size_t len);
sds *sdssplitargs(const char *line, int *argc);
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);
sds sdsjoin(char **argv, int argc, char *sep);
sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen);

/* Low level functions exposed to the user API */
sds sdsMakeRoomFor(sds s, size_t addlen);
void sdsIncrLen(sds s, ssize_t incr);
sds sdsRemoveFreeSpace(sds s);
size_t sdsAllocSize(sds s);
void *sdsAllocPtr(sds s);

/* Export the allocator used by SDS to the program using SDS.
 * Sometimes the program SDS is linked to, may use a different set of
 * allocators, but may want to allocate or free things that SDS will
 * respectively free or allocate. */
void *sds_malloc(size_t size);
void *sds_realloc(void *ptr, size_t size);
void sds_free(void *ptr);

#ifdef REDIS_TEST
int sdsTest(int argc, char *argv[]);
#endif

#endif
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容