Redis源碼閱讀筆記(1)-簡單動態(tài)字符串SDS

字符串是Redis中一個重要的組成部分,Redis沒有直接使用C語言自帶的字符串,而是自身構(gòu)建了一個簡單動態(tài)字符串(Simple dynamic string, SDS)的抽象類型,該抽象類型不僅有額外的特性,還能兼容部分C語言內(nèi)建的字符串操作函數(shù)。

涉及的主要源代碼文件

  1. sds.h
  2. sds.c

SDS的定義

typedef char *sds;      //聲明一個字符串指針類型的別名

//動態(tài)字符串結(jié)構(gòu)
//總長度 = len + free + 1  (1是末尾字符串\0所占的字節(jié))
struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];        //flexible array member,在計算struct長度是不算在內(nèi)
};
SDS示例

SDS字符串與C字符串相比,在結(jié)構(gòu)體中定義lenfree屬性,len用來記錄當前buf數(shù)組中已使用的字節(jié)空間,free記錄了buf數(shù)組中未使用的字符串空間。SDS字符串與C字符串一樣,使用了\0作為字符串結(jié)尾,但給\0字符不算入len中,因此buf數(shù)組的總大小應(yīng)為total = len + free + 1。如圖中Redis的SDS字符串buf分配的空間為10字節(jié)。\0字節(jié)的添加完全有SDS底層函數(shù)負責,使用者無需關(guān)心,也由于這個\0字符的存在,使得SDS可以重用一部分C字符串函數(shù)。

備注:sizeof(sdshdr) = 2 * sizeof(int64),buf所占的空間為0,可參考flexible array member

SDS的關(guān)鍵實現(xiàn)細節(jié)

//得到動態(tài)字符串鎖保存字符串的長度(不含\0)
static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}
//得到動態(tài)字符串的可用長度
static inline size_t sdsavail(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->free;
}

由于SDS中記錄了自身的長度len,因此獲取字符串長度的時間復(fù)雜度為O(1),而不是C字符串的O(N),因此提高了獲取字符串長度的性能。從上面可以看到,lenfree的獲取需要通過指針計算來獲取sdshdr的實際地址來獲得的。


//指定長度初始化sds,init表示初始化填寫的內(nèi)容,init為空表示初始化字符串長度為0
sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;

    //sdshdr的長度: strlen(sdshdr) + initlen + 1(1表示末尾0所占的資費)
    if (init) {
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);      
    } else {
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);      //初始化為0
    }
    if (sh == NULL) return NULL;
    sh->len = initlen;
    sh->free = 0;
    if (initlen && init)
        memcpy(sh->buf, init, initlen);                     //復(fù)制init指向地址的字符串
    sh->buf[initlen] = '\0';
    return (char*)sh->buf;                                  //sds的指針為實際字符串的指針
}

sdsnewlen的返回可以看到,實際返回給上層的是buf數(shù)組的地址,對上層屏蔽了sdshdrsdshdr的地址可以通過sdshder.buf的地址來算出。


//增加sds的額外可存儲空間至addlen字節(jié),free = addlen
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;    //當free空間足夠時,直接返回,無需重分配
    len = sdslen(s);
    sh = (void*) (s-(sizeof(struct sdshdr)));
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)      //預(yù)分配策略,若新字符長度小于1M,則為字符串分配2倍所需空間的大小
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;     //否則,只額外添加1M未使用空間
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
    if (newsh == NULL) return NULL;

    newsh->free = newlen - len;
    return newsh->buf;
}

//字符串左右修剪函數(shù)
sds sdstrim(sds s, const char *cset) {
    struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
    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 > start && strchr(cset, *ep)) ep--;    //修建右邊
    len = (sp > ep) ? 0 : ((ep-sp)+1);
    if (sh->buf != sp) memmove(sh->buf, sp, len); //移動內(nèi)存存儲內(nèi)容
    //更新末尾、len、free
    sh->buf[len] = '\0';
    sh->free = sh->free+(sh->len-len);
    sh->len = len;
    return s;
}

//獲取子字符串,start/end 允許傳負數(shù)
void sdsrange(sds s, int start, int end) {
    struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
    size_t newlen, len = sdslen(s);

    if (len == 0) return;
    if (start < 0) {
        start = len+start;
        if (start < 0) start = 0;
    }
    if (end < 0) {
        end = len+end;
        if (end < 0) end = 0;
    }
    newlen = (start > end) ? 0 : (end-start)+1;
    if (newlen != 0) {
        if (start >= (signed)len) {
            newlen = 0;
        } else if (end >= (signed)len) {
            end = len-1;
            newlen = (start > end) ? 0 : (end-start)+1;
        }
    } else {
        start = 0;
    }
    if (start && newlen) memmove(sh->buf, sh->buf+start, newlen);
    sh->buf[newlen] = 0;
    sh->free = sh->free+(sh->len-newlen);
    sh->len = newlen;
}

由于SDS字符串存在未使用空間,因此修改字符串長度不像C字符串,無需頻繁的通過內(nèi)存重分配來擴展或縮小字符串所占大小。這對于需要頻繁修改數(shù)據(jù)的Redis是一個極大的性能提升sdsMakeRoomFor函數(shù)用來對SDS字符串進行擴展,sdstrimsdsrange用來對字符串進行縮減。

通過控制未使用空間,SDS實現(xiàn)了空間預(yù)分配惰性釋放兩種空間優(yōu)化策略。

  1. 空間預(yù)分配
    若對字符串進行擴展后的大小(newlen)小于1M (1024*1024字節(jié)),那么給SDS字符串額外分配所需大小一倍(擴展后大小為2*newlen)的空間。若對字符串進行擴展后的大小大于1M,則給SDS字符串額外分配1M空間。通過這種方式,減少了執(zhí)行字符串增長操作所需的內(nèi)存分配次數(shù)。

  2. 惰性釋放
    當SDS字符串進行縮減時,并不釋放多出來的空間,而是通過修改free屬性和len屬性來表示字符串的縮減,寵兒頻繁的內(nèi)存操作。


//連接指定長度字符串到sds末尾
sds sdscatlen(sds s, const void *t, size_t len) {
    struct sdshdr *sh;
    size_t curlen = sdslen(s);

    s = sdsMakeRoomFor(s,len);          //擴展空間
    if (s == NULL) return NULL;
    sh = (void*) (s-(sizeof(struct sdshdr)));
    memcpy(s+curlen, t, len);
    sh->len = curlen+len;
    sh->free = sh->free-len;
    s[curlen+len] = '\0';           //末尾添加0
    return s;
}

SDS不使用C字符串函數(shù)的strcat函數(shù),而是自己封裝了一個,當free空間不足時,會擴展SDS字符串的未使用空間,然后在進行拼接,從而避免了緩沖區(qū)溢出

總結(jié)

SDS字符串相比C字符串的優(yōu)勢:

  1. O(1)獲取字符串長度;
  2. 通過空間預(yù)分配和惰性空間釋放減少內(nèi)存的操作次數(shù);
  3. 杜絕緩沖區(qū)溢出
  4. 因為是通過len來控制字符串的長度,不依賴于\0,因此字符串是二進制安全的,不僅可以保存文本數(shù)據(jù),還可以用來保存任意格式的二進制數(shù)據(jù)。
  5. 因為SDS字符串末尾帶有\0,因此作為存儲普通字符串,可以使用部分C語言字符串函數(shù)。
SDS主要API
function description time complexity
sdslen 獲取sds字符串長度 O(1)
sdsavail 獲取sds可用長度 O(1)
sdsnewlen 創(chuàng)建指定長度的sds,并接受C字符串為初始化內(nèi)容 O(N)
sdsnew 根據(jù)給定的C字符串創(chuàng)建sds O(N)
sdsempty 創(chuàng)建一個空的sds O(1)
sdsdup 復(fù)制sds O(N)
sdsfree 釋放sds O(1)
sdsgrowzero 將sds擴展到指定長度,新擴展的內(nèi)容用\0賦值 O(N)
sdscatlen 將一個給定字符串復(fù)制指定長度到sds末尾 O(N)
sdscat 將一個給定字符串添加到sds末尾 O(n)
sdscatsds 將一個sds添加到sds末尾 O(N)
sdscpylen 將一個給定字符串復(fù)制一定長度到sds中 O(N)
sdscpy 將一個給定字符串復(fù)制到sds中 O(N)
sdstrim 修剪sds O(M*N),M為sds長度,N為修剪內(nèi)容長度
sdsrange 保留給定sds一定范圍的長度 O(N)
sdsupdatelen 重新計算sds文本字符串的長度 O(N)
sdsclear 清除sds為空字符串 O(1)
sdscmp 比較sds字符串 O(N)
sdstolower sds字符串轉(zhuǎn)為小寫 O(N)
sdstoupper sds字符串轉(zhuǎn)為大寫 O(N)
Reference
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

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