Redis SDS源碼解析,從1st commit到5.0.4(最新版)的演化

0、引言

Redis沒有直接使用C語言傳統(tǒng)的字符串表示(以'\0'結(jié)尾的字符數(shù)組),而是構(gòu)建了一種名為簡單動態(tài)字符串(simple dynamic string, sds)的類型,用于Redis默認字符串的表示。

從2009年3月的1st commit到目前的最新版5.0.4,sds的變化相對是比較小的,中間只有一次較大的改動。commit記錄(PR#2509)如下:

PR#2509 commit記錄

主要是優(yōu)化了內(nèi)存使用、提高了字符串最大長度。該修改最早出現(xiàn)在3.2.0 RC1(Redis 3.2.0 RC1 (version 3.1.101) )Release date: 23 dec 2015):

* [NEW] SDS improvements for speed and maximum string length.
        This makes Redis more memory efficient in different use cases.
        (Design and implementation by Oran Agra, some additional work
         by Salvatore Sanfilippo)

本文嘗試從源碼角度解析Redis SDS的設(shè)計、實現(xiàn)和演化。

1、SDS的定義

這里的定義是PR#2509之前的定義:

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

len:記錄buf數(shù)組中已使用字節(jié)的數(shù)量,不計'\0'
free:記錄buf數(shù)組中未使用字節(jié)的數(shù)量,不計'\0'
buf:字節(jié)數(shù)組,用于保存字符串。

SDS遵循C字符串以'\0'結(jié)尾的慣例,這樣做的好處是:SDS可以直接重用一部分C字符串函數(shù)庫里面的函數(shù)。如:
printf("%s", s->buf);

sds內(nèi)存示意圖

2、SDS與C字符串的區(qū)別

SDS相比C字符串,在安全性、性能、功能性具有優(yōu)勢:
1、安全性:防止緩沖區(qū)溢出、二進制安全
2、性能:獲取字符串長度、空間預(yù)分配、惰性釋放
3、功能性:二進制安全、兼容部分C字符串函數(shù)

2.1、緩沖區(qū)溢出

緩沖區(qū)溢出(buffer overflow):是這樣的一種異常,當(dāng)程序?qū)?shù)據(jù)寫入緩沖區(qū)時,會超過緩沖區(qū)的邊界,并覆蓋相鄰的內(nèi)存位置。

C字符串不記錄自身長度,不會自動進行邊界檢測,增加了溢出風(fēng)險。如下面這種情形:
char* strcat(char* dest, const char* src);

s1 = 'Redis',s2 = 'MongoDB',當(dāng)執(zhí)行strcat(s1, " Cluster")時,未給s1分配足夠內(nèi)存空間,s1的數(shù)據(jù)將溢出到s2所在的內(nèi)存空間,導(dǎo)致s2保存的內(nèi)容被意外地修改。

內(nèi)存溢出示意圖1

內(nèi)存溢出示意圖2

而SDS記錄了自身長度,同時在修改時,API會按照如下步驟進行:
1)先檢查SDS的空間是否滿足修改所需的要求;
2)如果不滿足要求的話,API會自動將SDS的空間擴展至執(zhí)行修改所需的大小(realloc);
3)然后才執(zhí)行實際的修改操作。
例子可見:sds.c/sdscat

2.2、二進制安全

二進制安全(binary-safe):指能處理任意的二進制數(shù)據(jù),包括非ASCII和null字節(jié)。

C字符串中的字符必須符合某種編碼(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否則最先被程序讀入的空字符會被誤認為是字符串的結(jié)尾。這些限制使得C字符串只能保存文本數(shù)據(jù),不能保存圖像、視頻、音頻等二進制數(shù)據(jù)。

而SDS不會對數(shù)據(jù)做任何限制、過濾、假設(shè),數(shù)據(jù)在寫入時是什么樣子的,它被讀取時就是什么樣的。因此Redis不僅可以保存文本數(shù)據(jù),還可以保存任意格式的二進制數(shù)據(jù)。

2.3、獲取字符串長度

C字符串并不記錄自身的長度信息,因此為了獲取一個C字符串的長度,必須遍歷整個字符串,直至'\0',其復(fù)雜度時O(n)。

而SDS記錄了自身長度len,因此通過O(1)復(fù)雜度就能獲取字符串的長度。

2.4、空間預(yù)分配

當(dāng)SDS的API對一個SDS進行修改,并且需要對SDS進行空間擴展的時候,程序不僅會為SDS分配修改所必須的空間,還會為SDS分配額外的未使用空間。具體策略見sds.c/sdsMakeRoomFor

/* 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) {
    struct sdshdr *sh, *newsh;
    size_t free = sdsavail(s);
    size_t len, newlen;
    if (free >= addlen) return s;
    len = sdslen(s);
    sh = (void*) (s-(sizeof(struct sdshdr)));
    newlen = (len+addlen);

    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;

    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
    if (newsh == NULL) return NULL;
    newsh->free = newlen - len;
    return newsh->buf;
}

#define SDS_MAX_PREALLOC (1024*1024)
2.5、惰性釋放

當(dāng)SDS的API需要縮短SDS保存的字符串時,程序并不立即使用內(nèi)存重分配來回收縮短后多出來的字節(jié),而是使用free屬性將這些字節(jié)的數(shù)量記錄起來,并等待將來使用。

惰性釋放示意圖1

惰性釋放示意圖1

下面這個api是將sds置空,但并未真正釋放內(nèi)存:

/* Modify an sds string in-place to make it empty (zero length).
* However all the existing buffer is not discarded but set as free space
* so that next append operations will not require allocations up to the
* number of bytes previously available. */
void sdsclear(sds s) {
    struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
    sh->free += sh->len;
    sh->len = 0;
    sh->buf[0] = '\0';
}

真正釋放sds內(nèi)存的api:

/* Free an sds string. No operation is performed if 's' is NULL. */
void sdsfree(sds s) {
    if (s == NULL) return;
    zfree(s-sizeof(struct sdshdr));
}

回收free內(nèi)存的api:

/* Reallocate the sds string so that it has no free space at the end. The
* contained string remains not altered, but next concatenation operations
* will require a reallocation.
*
* After the call, the passed sds string is no longer valid and all the
* references must be substituted with the new pointer returned by the call. */
sds sdsRemoveFreeSpace(sds s) {
    struct sdshdr *sh;
    sh = (void*) (s-(sizeof(struct sdshdr)));
    sh = zrealloc(sh, sizeof(struct sdshdr)+sh->len+1);
    sh->free = 0;
    return sh->buf;
}
2.6、兼容部分C字符串函數(shù)

SDS的buf數(shù)組會以'\0'結(jié)尾,這樣SDS就可以重用C字符串函數(shù)庫里的一些函數(shù),避免了不必要的代碼重復(fù)。

3、SDS的演化

這里主要介紹的是PR#2509和緊接著的antirez的“New sds type 5 implemented”。


commit記錄

sds的定義變?yōu)椋?/p>

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

首先,sdshdr結(jié)構(gòu)體由1個變成了5個。__attribute__ ((__packed__))的作用就是告訴編譯器取消結(jié)構(gòu)體在編譯過程中的優(yōu)化對齊,按照實際占用字節(jié)數(shù)進行對齊。

在解釋新的SDS結(jié)構(gòu)為何能優(yōu)化內(nèi)存前,先看一個測試:

#include <iostream>
#include <cstdint>
using namespace std;

struct sdshdr
{
    unsigned int len;
    unsigned int free;
    char buf[];
};

struct sdshdr2
{
    unsigned int len;
    unsigned int free;
};

struct sdshdr3
{
    char buf[];
};

int main() {
    cout << sizeof(struct sdshdr) << endl;           // 8
    cout << sizeof(unsigned int) << endl << endl;    // 4
    
    cout << sizeof(uint8_t) << endl;                 // 1
    cout << sizeof(uint16_t) << endl;                // 2
    cout << sizeof(uint32_t) << endl;                // 4
    cout << sizeof(uint64_t) << endl << endl;        // 8
    
    cout << sizeof(struct sdshdr2) << endl;   // 8
    cout << sizeof(struct sdshdr3) << endl;   // 1
    cout << sizeof(char) << endl;             // 1
    
    return 0;
}

typedef unsigned int uint32_t;

為什么要如此優(yōu)化,或者說之前的sds結(jié)構(gòu)存在什么問題呢?

之前sds存在的問題:
1)浪費內(nèi)存。共8 bytes header,即使很短的字符串也需要4字節(jié)的len字段
2)字符串大小上限4G。len最大值:2^32 - 1,字符串大小上限:2^32 / 1024 / 1024 / 1024 = 4G

PR#2509的改進:
sdshdr8 3 bytes header
sdshdr16 5 bytes header
sdshdr32 9 bytes header
sdshdr64 17 bytes header

在antirez的“New sds type 5 implemented”提交中,有2點修改:
1)修改了flags

char flags; /* 2 lsb of type, and 6 msb of refcount */

unsigned char flags; /* 3 lsb of type, and 5 msb of string length */

oranagra的PR用2位表示類型,antirez則認為,大多數(shù)SDS字符串從未調(diào)整大小,并且最小的它們不太可能調(diào)整大小,因此可以使用3位代替。oranagra設(shè)想其他幾位可以用作引用計數(shù),antirez則暫時放棄了這種考慮,暫時將這些bit預(yù)留,可能用于非常小的字符串的額外編碼。

2)增加了sdshdr5

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};

用低3位表示類型,剩余5位表示sds字符串長度,那么可以表示長度為31(2^5-1)的字符串,這種情況下,最多才1個字節(jié)的header。如果需要對該字符串調(diào)整大小,可以直接升級到下一級(SDS_TYPE_8,3 bytes header)。

下面看新的sds結(jié)構(gòu)怎么用,以sdslen為例:

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

SDS_TYPE_32,類型為3,二進制0011
SDS_TYPE_MASK,mask為7,二進制0111

0011
& 0111
————
0011

作者驗證了速度提升在大多數(shù)命令中都不是很明顯,除了具有依賴于大量sdscatlen()或sdcatfmt()的工作負載的命令,其中可以使用高達25%的性能。但最重要的是這次改進添加了很重要的一項功能:為Redis提供了超過4G字符串的能力。

本文的不足之處,未對修改前后內(nèi)存對齊的問題做研究分析,參考資料3中有相關(guān)討論。對于該內(nèi)容,是以后學(xué)習(xí)的一個方向。

參考資料:

1、3.2 release note

2、f15df8b commit記錄

3、sds size classes - memory optimization #2509(記錄了antirez和oranagra關(guān)于優(yōu)化的詳細對話細節(jié))

4、Redis設(shè)計與實現(xiàn) (黃建宏)

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

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)載:可能是目前最詳細的Redis內(nèi)存模型及應(yīng)用解讀 Redis是目前最火爆的內(nèi)存數(shù)據(jù)庫之一,通過在內(nèi)存中讀寫數(shù)據(jù)...
    meng_philip123閱讀 1,445評論 1 22
  • 前言 Redis是目前最火爆的內(nèi)存數(shù)據(jù)庫之一,通過在內(nèi)存中讀寫數(shù)據(jù),大大提高了讀寫速度,可以說Redis是實現(xiàn)網(wǎng)站...
    小陳阿飛閱讀 809評論 0 1
  • 轉(zhuǎn)載:可能是目前最詳細的Redis內(nèi)存模型及應(yīng)用解讀 Redis是目前最火爆的內(nèi)存數(shù)據(jù)庫之一,通過在內(nèi)存中讀寫數(shù)據(jù)...
    jwnba24閱讀 628評論 0 4
  • 廣州CBD中心,繁華的夜景,我愛慘了這般景象。 5月6日,決定成為廣漂一族,不顧一切,甚至爸媽都不...
    糖果冰淇淋閱讀 412評論 0 1
  • 你黑色的羽毛在風(fēng)中搖曳 你黑色的眼睛在暗夜中尋覓 你在等待著什么 還是守護著什么 老樹下的枯井 鋪滿了黑色的落葉 ...
    砸西瓜閱讀 184評論 0 3