Redis的數(shù)據(jù)結(jié)構(gòu)(一):簡(jiǎn)單動(dòng)態(tài)字符串

開場(chǎng)白

作為redis的使用者渐裸,我們最常用的操作可能就是set了摊腋,而最常用的數(shù)據(jù)結(jié)構(gòu)則是string了昙沦,我們常常會(huì)使用命令:

127.0.0.1:6379> set key value

這個(gè)命令中,key和value都被保存到一個(gè)字符串中舍杜。需要注意的是新娜,這個(gè)字符串并非C語(yǔ)言的字符串結(jié)構(gòu),而是redis自己實(shí)現(xiàn)的一個(gè)名為sds的數(shù)據(jù)結(jié)構(gòu)

SDS的定義

我們翻到redis3.0的源碼既绩,找到src/sds.h,先看看這個(gè)sds的結(jié)構(gòu)體定義:

/*
 * 保存字符串對(duì)象的結(jié)構(gòu)
 */
struct sdshdr {
    
    // buf 中已占用空間的長(zhǎng)度
    int len;

    // buf 中剩余可用空間的長(zhǎng)度
    int free;

    // 數(shù)據(jù)空間
    char buf[];
};
  • 字段free:表示可分配的空間的字節(jié)長(zhǎng)度
  • 字段len:表示已占用空間的字節(jié)長(zhǎng)度
  • 字段buf:表示裝載實(shí)際字符串的字符數(shù)組

舉一個(gè)最簡(jiǎn)單的例子,比如Redis這個(gè)字符串还惠,在sds的保存則是這樣的:

image.png

為什么要在后面加上這個(gè)\0呢饲握?這么設(shè)計(jì)的目標(biāo)是為了遵循C語(yǔ)言字符結(jié)尾的慣例,同時(shí)能夠兼容一部分C語(yǔ)言字符串操作庫(kù)函數(shù)蚕键,比如strlen救欧。同時(shí)這個(gè)\0是不會(huì)記錄進(jìn)去len字段的。

使用SDS的好處

1.有效提升處理長(zhǎng)字符串的性能

C語(yǔ)言的字符串類型锣光,計(jì)算長(zhǎng)度是必須要遍歷字符的笆怠,也就是如果長(zhǎng)度為5的字符串,那么計(jì)算長(zhǎng)度必須要遍歷五次才能得出結(jié)果誊爹,計(jì)算復(fù)雜度是O(N)蹬刷。如果使用了sds的話,可以在分配字符串的時(shí)候就直接把長(zhǎng)度記錄在len字段频丘,那么這個(gè)計(jì)算復(fù)雜度將會(huì)是O(1)办成,這么做能夠解決redis處理大型字符串長(zhǎng)度的性能瓶頸問(wèn)題。

2.防止緩沖區(qū)溢出搂漠,優(yōu)化內(nèi)存分配

普通的C語(yǔ)言字符串會(huì)存在緩沖區(qū)溢出的問(wèn)題迂卢,舉個(gè)最簡(jiǎn)單的例子,如字符串s1是redis,字符串s2是mongodb而克,并假設(shè)他們?cè)趦?nèi)存中是緊緊相鄰的靶壮。假如一個(gè)粗心的程序員沒(méi)有為s1去分配更多空間的前提下就調(diào)用了strcat進(jìn)行了字符串拼接,如strcat(s1, "cluster")员萍,那么這個(gè)s1就會(huì)溢出并且可能擦寫s2的內(nèi)存亮钦,就會(huì)出現(xiàn)一些很坑爹但又難以發(fā)現(xiàn)的bug了。但是使用sds就能規(guī)避這個(gè)問(wèn)題充活,比如src/sds.c文件中有一個(gè)函數(shù)叫sdscat,我們看一下代碼

sds sdscat(sds s, const char *t) {
    return sdscatlen(s, t, strlen(t));
}

sds sdscatlen(sds s, const void *t, size_t len) {
    
    struct sdshdr *sh;
    
    // 原有字符串長(zhǎng)度
    size_t curlen = sdslen(s);

    // 擴(kuò)展 sds 空間
    // T = O(N)
    s = sdsMakeRoomFor(s,len);

    // 內(nèi)存不足蜂莉?直接返回
    if (s == NULL) return NULL;

    // 復(fù)制 t 中的內(nèi)容到字符串后部
    // T = O(N)
    sh = (void*) (s-(sizeof(struct sdshdr)));
    memcpy(s+curlen, t, len);

    // 更新屬性
    sh->len = curlen+len;
    sh->free = sh->free-len;

    // 添加新結(jié)尾符號(hào)
    s[curlen+len] = '\0';

    // 返回新 sds
    return s;
}

// 著重看這個(gè)函數(shù)
sds sdsMakeRoomFor(sds s, size_t addlen) {

    struct sdshdr *sh, *newsh;

    // 獲取 s 目前的空余空間長(zhǎng)度
    size_t free = sdsavail(s);

    size_t len, newlen;

    // s 目前的空余空間已經(jīng)足夠,無(wú)須再進(jìn)行擴(kuò)展混卵,直接返回
    if (free >= addlen) return s;

    // 獲取 s 目前已占用空間的長(zhǎng)度
    len = sdslen(s);
    sh = (void*) (s-(sizeof(struct sdshdr)));

    // s 最少需要的長(zhǎng)度
    newlen = (len+addlen);

    // 根據(jù)新長(zhǎng)度映穗,為 s 分配新空間所需的大小
    if (newlen < SDS_MAX_PREALLOC)
        // 如果新長(zhǎng)度小于 SDS_MAX_PREALLOC 
        // 那么為它分配兩倍于所需長(zhǎng)度的空間
        newlen *= 2;
    else
        // 否則,分配長(zhǎng)度為目前長(zhǎng)度加上 SDS_MAX_PREALLOC
        newlen += SDS_MAX_PREALLOC;
    // T = O(N)
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);

    // 內(nèi)存不足幕随,分配失敗蚁滋,返回
    if (newsh == NULL) return NULL;

    // 更新 sds 的空余長(zhǎng)度
    newsh->free = newlen - len;

    // 返回 sds
    return newsh->buf;
}

我們著重看看sdsMakeRoomFor函數(shù),它展示了redis的sds是如何分配內(nèi)存的赘淮,還是舉個(gè)栗子:我們有字符串s1="redis"辕录,我們調(diào)用sdscat(s1, "cluster"),那么s1會(huì)變?yōu)?code>redis cluster梢卸,因?yàn)樵瓉?lái)的s1的len字段只有5字節(jié)走诞,因此連接cluster需要進(jìn)行內(nèi)存擴(kuò)展操作,可以從代碼看到蛤高,如果s1的free的長(zhǎng)度大于等于cluster的長(zhǎng)度蚣旱,那么會(huì)把free這部分讓出來(lái)進(jìn)行分配給cluster字符串,但如果不夠長(zhǎng)又會(huì)產(chǎn)生兩種分配策略:

  1. 如果連接后新的字符串的長(zhǎng)度尚未超過(guò)宏定義SDS_MAX_PREALLOC(默認(rèn)是1MB)戴陡,那么會(huì)分配兩倍于新字符串的長(zhǎng)度塞绿。比如連接后的字符串s1是redis cluster,長(zhǎng)度即是13恤批,那么會(huì)多分配出13個(gè)字節(jié)异吻,并把長(zhǎng)度保存在free字段,那么最終擴(kuò)容后buf的空間就是13 + 13 + 1 = 27個(gè)字節(jié)喜庞。
  2. 如果連接后的字符串長(zhǎng)度超過(guò)宏定義SDS_MAX_PREALLOC(默認(rèn)是1MB)诀浪,那么會(huì)比新字符串長(zhǎng)度分配多SDS_MAX_PREALLOC(默認(rèn)是1MB)的內(nèi)存。比如連接后的字符串s1長(zhǎng)度是10MB赋荆,那么buf擴(kuò)容分配后的空間就是30MB + 1MB + 1Byte笋妥。

除此之外,redis的sds api也有惰性釋放的機(jī)制窄潭。假如一個(gè)字符串需要被裁減春宣,比如redis cluster變?yōu)?code>redis酵颁,sds的api不會(huì)馬上把cluster這7個(gè)字節(jié)空間馬上釋放掉,而是會(huì)繼續(xù)保留月帝,并記錄在free字段中躏惋,也就是free=7,這么做是為了日后如果redis這個(gè)字符串要增長(zhǎng)的時(shí)候就不用去重新分配內(nèi)存了嚷辅。當(dāng)然簿姨,如果你真的覺(jué)得這部分空間用不著了,也可以通過(guò)sdsRemoveFreeSpace這個(gè)函數(shù)去釋放掉這部分free的空間簸搞。

3.保證二進(jìn)制安全

C語(yǔ)言字符串必須符合某種編碼扁位,并且除了字符串末尾之外,字符串里面不能包含空字符串趁俊,否則會(huì)認(rèn)為是結(jié)尾了域仇,因此也導(dǎo)致C語(yǔ)言字符串沒(méi)辦法保存、圖片寺擂、視頻暇务、壓縮文件等二進(jìn)制數(shù)據(jù)。
redis的sds使用了字符數(shù)組作為字符串的保存結(jié)構(gòu)的好處就顯現(xiàn)出來(lái)了怔软,正因?yàn)槭亲址麛?shù)組垦细,那么可以用來(lái)保存任何的二進(jìn)制數(shù)據(jù),并且保證讀入數(shù)據(jù)是什么挡逼,讀出的數(shù)據(jù)就是什么括改。

以上就是redis簡(jiǎn)單動(dòng)態(tài)字符串的特點(diǎn)和優(yōu)勢(shì)的總結(jié),后面會(huì)繼續(xù)redis的其他數(shù)據(jù)結(jié)構(gòu)挚瘟,敬請(qǐng)期待叹谁!

文章參考自:<<Redis設(shè)計(jì)與實(shí)現(xiàn)>> 第二版

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市乘盖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌憔涉,老刑警劉巖订框,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異兜叨,居然都是意外死亡穿扳,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門国旷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)矛物,“玉大人,你說(shuō)我怎么就攤上這事跪但÷男撸” “怎么了?”我有些...
    開封第一講書人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)忆首。 經(jīng)常有香客問(wèn)我爱榔,道長(zhǎng),這世上最難降的妖魔是什么糙及? 我笑而不...
    開封第一講書人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任详幽,我火速辦了婚禮,結(jié)果婚禮上浸锨,老公的妹妹穿的比我還像新娘唇聘。我一直安慰自己,他們只是感情好柱搜,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開白布迟郎。 她就那樣靜靜地躺著,像睡著了一般冯凹。 火紅的嫁衣襯著肌膚如雪谎亩。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評(píng)論 1 291
  • 那天宇姚,我揣著相機(jī)與錄音匈庭,去河邊找鬼。 笑死浑劳,一個(gè)胖子當(dāng)著我的面吹牛阱持,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播魔熏,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼衷咽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蒜绽?” 一聲冷哼從身側(cè)響起镶骗,我...
    開封第一講書人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎躲雅,沒(méi)想到半個(gè)月后鼎姊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡相赁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年相寇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钮科。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡唤衫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绵脯,到底是詐尸還是另有隱情佳励,我是刑警寧澤休里,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布,位于F島的核電站植兰,受9級(jí)特大地震影響份帐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜楣导,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一废境、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧筒繁,春花似錦噩凹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至呕缭,卻和暖如春堵泽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背恢总。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工迎罗, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人片仿。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓纹安,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親砂豌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子厢岂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351