1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        深挖 Redis 6.0 源碼—— SDS

        共 5001字,需瀏覽 11分鐘

         ·

        2020-08-21 06:17

        本文精選自 Doocs 開(kāi)源社區(qū)旗下“源碼獵人”項(xiàng)目,作者楊立濱。

        項(xiàng)目將會(huì)持續(xù)更新,歡迎 Star 關(guān)注。

        項(xiàng)目地址:https://github.com/doocs/source-code-hunter

        SDS(Simple Dynamic Strings, 簡(jiǎn)單動(dòng)態(tài)字符串)是 Redis 的一種基本數(shù)據(jù)結(jié)構(gòu),主要是用于存儲(chǔ)字符串和整數(shù)。?這篇文章里,我們就來(lái)探討一下 Redis SDS 這種數(shù)據(jù)結(jié)構(gòu)的底層實(shí)現(xiàn)原理。

        學(xué)習(xí)之前,首先我們要明確,Redis 是一個(gè)使用 C 語(yǔ)言編寫(xiě)的鍵值對(duì)存儲(chǔ)系統(tǒng)

        前置思考

        我們首先考慮一個(gè)問(wèn)題,如何實(shí)現(xiàn)一個(gè)二進(jìn)制安全的字符串?

        在 C 語(yǔ)言中,\0?表示字符串結(jié)束,如果字符串中本身就包含?\0?字符,那么字符串就會(huì)在?\0?處被截?cái)?,即非二進(jìn)制安全;若通過(guò)使用一個(gè) len 屬性,來(lái)判斷字符串是否結(jié)束,就可以保證讀寫(xiě)字符串時(shí)不受到?\0?的影響,則是二進(jìn)制安全。同時(shí) len 屬性也能保證在 O(1) 時(shí)間內(nèi)獲取字符串的長(zhǎng)度。

        Redis 3.2 以前的 SDS 實(shí)現(xiàn)

        在 Redis 3.2 版本以前,SDS 的結(jié)構(gòu)如下:

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

        其中,buf 表示數(shù)據(jù)空間,用于存儲(chǔ)字符串;len 表示 buf 中已占用的字節(jié)數(shù),也即字符串長(zhǎng)度;free 表示 buf 中剩余可用字節(jié)數(shù)。

        字段 len 和 free 各占 4 字節(jié),緊接著存放字符串。

        這樣做有以下幾個(gè)好處:

        ?用單獨(dú)的變量 len 和 free,可以方便地獲取字符串長(zhǎng)度和剩余空間;?內(nèi)容存儲(chǔ)在動(dòng)態(tài)數(shù)組 buf 中,SDS 對(duì)上層暴露的指針指向 buf,而不是指向結(jié)構(gòu)體 SDS。因此,上層可以像讀取 C 字符串一樣讀取 SDS 的內(nèi)容,兼容 C 語(yǔ)言處理字符串的各種函數(shù),同時(shí)也能通過(guò) buf 地址的偏移,方便地獲取其他變量;?讀寫(xiě)字符串不依賴(lài)于?\0,保證二進(jìn)制安全。

        但其實(shí)以上的設(shè)計(jì)是存在一些問(wèn)題的,對(duì)于不同長(zhǎng)度的字符串,是否有必要使用 len 和 free 這 2 個(gè) 4 字節(jié)的變量?4 字節(jié)的 len,可表示的字符串長(zhǎng)度為?2^32,而在實(shí)際應(yīng)用中,存放于 Redis 中的字符串往往沒(méi)有這么長(zhǎng),因此,空間的使用上能否進(jìn)一步壓縮?

        那么接下來(lái),我們就來(lái)看看最新的 Redis 是如何根據(jù)字符串的長(zhǎng)度,使用不同的數(shù)據(jù)結(jié)構(gòu)進(jìn)行存儲(chǔ)的。

        Redis SDS 最新實(shí)現(xiàn)

        在 Redis 3.2 版本之后(v3.2 - v6.0),Redis 將 SDS 劃分為 5 種類(lèi)型:

        ?sdshdr5:長(zhǎng)度小于 1 字節(jié)?sdshdr8:長(zhǎng)度 1 字節(jié)?sdshdr16:長(zhǎng)度 2 字節(jié)?sdshdr32:長(zhǎng)度 4 字節(jié)?sdshdr64:長(zhǎng)度 8 字節(jié)

        Redis 增加了一個(gè) flags 字段來(lái)標(biāo)識(shí)類(lèi)型,用一個(gè)字節(jié)(8 位)來(lái)存儲(chǔ)。

        其中:前 3 位表示字符串的類(lèi)型;剩余 5 位,可以用來(lái)存儲(chǔ)長(zhǎng)度小于 32 的短字符串。

        struct __attribute__ ((__packed__)) sdshdr5 {    unsigned char flags; /* 前3位存儲(chǔ)類(lèi)型,后5位存儲(chǔ)長(zhǎng)度 */    char buf[]; /* 動(dòng)態(tài)數(shù)組,存放字符串 */};

        而對(duì)于長(zhǎng)度大于 31 的字符串,僅僅靠 flags 的后 5 位來(lái)存儲(chǔ)長(zhǎng)度明顯是不夠的,需要用另外的變量來(lái)存儲(chǔ)。sdshdr8、sdshdr16、sdshdr32、sdshdr64 的數(shù)據(jù)結(jié)構(gòu)定義如下,其中 len 表示已使用的長(zhǎng)度,alloc 表示總長(zhǎng)度,buf 存儲(chǔ)實(shí)際內(nèi)容,而 flags 的前 3 位依然存儲(chǔ)類(lèi)型,后 5 位則預(yù)留。

        struct __attribute__ ((__packed__)) sdshdr8 {    uint8_t len; /* 已使用長(zhǎng)度,1字節(jié) */    uint8_t alloc; /* 總長(zhǎng)度,1字節(jié) */    unsigned char flags; /* 前3位存儲(chǔ)類(lèi)型,后5位預(yù)留 */    char buf[];};struct __attribute__ ((__packed__)) sdshdr16 {    uint16_t len; /* 已使用長(zhǎng)度,2字節(jié) */    uint16_t alloc; /* 總長(zhǎng)度,2字節(jié) */    unsigned char flags; /* 前3位存儲(chǔ)類(lèi)型,后5位預(yù)留 */    char buf[];};struct __attribute__ ((__packed__)) sdshdr32 {    uint32_t len; /* 已使用長(zhǎng)度,4字節(jié) */    uint32_t alloc; /* 總長(zhǎng)度,4字節(jié) */    unsigned char flags; /* 前3位存儲(chǔ)類(lèi)型,后5位預(yù)留 */    char buf[];};struct __attribute__ ((__packed__)) sdshdr64 {    uint64_t len; /* 已使用長(zhǎng)度,8字節(jié) */    uint64_t alloc; /* 總長(zhǎng)度,8字節(jié) */    unsigned char flags; /* 前3位存儲(chǔ)類(lèi)型,后5位預(yù)留 */    char buf[];};

        注意,一般情況下,結(jié)構(gòu)體會(huì)按照所有變量大小的最小公倍數(shù)做字節(jié)對(duì)齊,而用?packed?修飾后,結(jié)構(gòu)體則變?yōu)?1 字節(jié)對(duì)齊。這樣做的好處有二:一是節(jié)省內(nèi)存,比如 sdshdr32 可節(jié)約 3 個(gè)字節(jié);二是?SDS 返回給上層的是指向 buf 的指針,此時(shí)按 1 字節(jié)對(duì)齊,所以可在創(chuàng)建 SDS 后,通過(guò)?(char*)sh+hdrlen?得到 buf 指針地址,也可以通過(guò)?buf[-1]?找到 flags。

        以上,Redis 根據(jù)字符串長(zhǎng)度的不同,選擇對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)進(jìn)行存儲(chǔ)。接下來(lái),我們就來(lái)看看 Redis 字符串的相關(guān) API 實(shí)現(xiàn)。

        SDS API 實(shí)現(xiàn)

        1. 創(chuàng)建字符串

        sds sdsnewlen(const void *init, size_t initlen) {    void *sh;    sds s;    // 根據(jù)字符串長(zhǎng)度計(jì)算相應(yīng)類(lèi)型    char type = sdsReqType(initlen);    // 如果創(chuàng)建的是""字符串,強(qiáng)轉(zhuǎn)為SDS_TYPE_8    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;    // 根據(jù)類(lèi)型計(jì)算頭部所需長(zhǎng)度(頭部包含 len、alloc、flags)    int hdrlen = sdsHdrSize(type);    // 指向flags的指針    unsigned char *fp;    // 創(chuàng)建字符串,+1是因?yàn)?`\0` 結(jié)束符    sh = s_malloc(hdrlen+initlen+1);    if (sh == NULL) return NULL;    if (init==SDS_NOINIT)        init = NULL;    else if (!init)        memset(sh, 0, hdrlen+initlen+1);    // s指向buf    s = (char*)sh+hdrlen;    // s減1得到flags    fp = ((unsigned char*)s)-1;    ...    // 在s末尾添加\0結(jié)束符    s[initlen] = '\0';    // 返回指向buf的指針s    return s;}

        創(chuàng)建 SDS 的大致流程是這樣的:首先根據(jù)字符串長(zhǎng)度計(jì)算得到 type,根據(jù) type 計(jì)算頭部所需長(zhǎng)度,然后動(dòng)態(tài)分配內(nèi)存空間。

        注意:

        1.創(chuàng)建空字符串時(shí),SDS_TYPE_5?被強(qiáng)制轉(zhuǎn)換為?SDS_TYPE_8(原因是創(chuàng)建空字符串后,內(nèi)容可能會(huì)頻繁更新而引發(fā)擴(kuò)容操作,故直接創(chuàng)建為 sdshdr8)2.長(zhǎng)度計(jì)算有?+1?操作,因?yàn)榻Y(jié)束符?\0?會(huì)占用一個(gè)長(zhǎng)度的空間。3.返回的是指向 buf 的指針 s。

        2. 清空字符串

        SDS 提供了兩種清空字符串的方法。

        一種是通過(guò) s 偏移得到結(jié)構(gòu)體的地址,然后調(diào)用?s_free?直接釋放內(nèi)存。

        void sdsfree(sds s) {    if (s == NULL) return;    // s減去頭部的大小得到結(jié)構(gòu)體的地址    s_free((char*)s-sdsHdrSize(s[-1]));}

        另一種是通過(guò)重置 len 屬性值而達(dá)到清空字符串的目的,本質(zhì)上 buf 并沒(méi)有被真正清除,新的數(shù)據(jù)會(huì)直接覆蓋 buf 中原有的數(shù)據(jù),無(wú)需申請(qǐng)新的內(nèi)存空間。

        void sdsclear(sds s) {    // 將len屬性置為0    sdssetlen(s, 0);    s[0] = '\0';}

        3. 拼接字符串

        SDS 拼接字符串的實(shí)現(xiàn)如下:

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

        可以看到?sdscatsds?內(nèi)部調(diào)用的是?sdscatlen。

        而?sdscatlen?內(nèi)部的實(shí)現(xiàn)相對(duì)復(fù)雜一些,由于拼接字符串可能涉及 SDS 的擴(kuò)容,因此?sdscatlen?內(nèi)部調(diào)用?sdsMakeRoomFor?對(duì)拼接的字符串做檢查:若無(wú)需擴(kuò)容,直接返回 s;若需要擴(kuò)容,則返回?cái)U(kuò)容好的新字符串 s。

        sds sdscatlen(sds s, const void *t, size_t len) {    // 計(jì)算當(dāng)前字符串長(zhǎng)度    size_t curlen = sdslen(s);    // 確保s的剩余空間足以拼接上t    s = sdsMakeRoomFor(s,len);    if (s == NULL) return NULL;    // 拼接s、t    memcpy(s+curlen, t, len);    // 更新s的len屬性    sdssetlen(s, curlen+len);    // s末尾添加\0結(jié)束符    s[curlen+len] = '\0';    return s;}

        SDS 的擴(kuò)容策略是這樣的:

        1.若 SDS 中剩余空閑長(zhǎng)度 avail 大于或等于新增內(nèi)容的長(zhǎng)度 addlen,無(wú)需擴(kuò)容。2.若 SDS 中剩余空閑長(zhǎng)度 avail 小于或等于 addlen,則分情況討論:新增后總長(zhǎng)度?len+addlen < 1MB?的,按新長(zhǎng)度的 2 倍擴(kuò)容;新增后總長(zhǎng)度?len+addlen >= 1MB?的,按新長(zhǎng)度加上?1MB?擴(kuò)容。

        sds sdsMakeRoomFor(sds s, size_t addlen) {    void *sh, *newsh;    // 當(dāng)前剩余長(zhǎng)度    size_t avail = sdsavail(s);    size_t len, newlen;    char type, oldtype = s[-1] & SDS_TYPE_MASK;    int hdrlen;    /* 剩余長(zhǎng)度>=新增字符串長(zhǎng)度,直接返回 */    if (avail >= addlen) return s;    // 計(jì)算當(dāng)前字符串長(zhǎng)度len    len = sdslen(s);    sh = (char*)s-sdsHdrSize(oldtype);    // 計(jì)算新長(zhǎng)度    newlen = (len+addlen);    // 新長(zhǎng)度<1MB,按新長(zhǎng)度的2倍擴(kuò)容    if (newlen < SDS_MAX_PREALLOC)        newlen *= 2;    // 否則按新長(zhǎng)度+1MB擴(kuò)容    else        newlen += SDS_MAX_PREALLOC;    // 計(jì)算新長(zhǎng)度所屬類(lèi)型    type = sdsReqType(newlen);    /* type5不支持?jǐn)U容,強(qiáng)轉(zhuǎn)為type8 */    if (type == SDS_TYPE_5) type = SDS_TYPE_8;    hdrlen = sdsHdrSize(type);    if (oldtype==type) {        // 類(lèi)型沒(méi)變,直接通過(guò)realloc擴(kuò)大動(dòng)態(tài)數(shù)組即可。        newsh = s_realloc(sh, hdrlen+newlen+1);        if (newsh == NULL) return NULL;        s = (char*)newsh+hdrlen;    } else {        // 類(lèi)型改變了,則說(shuō)明頭部長(zhǎng)度也發(fā)生了變化,不進(jìn)行realloc操作,而是直接重新開(kāi)辟內(nèi)存        newsh = s_malloc(hdrlen+newlen+1);        if (newsh == NULL) return NULL;        // 原內(nèi)存拷貝到新的內(nèi)存地址上        memcpy((char*)newsh+hdrlen, s, len+1);        // 釋放原先空間        s_free(sh);        s = (char*)newsh+hdrlen;        // 為flags賦值        s[-1] = type;        // 為len屬性賦值        sdssetlen(s, len);    }    // 為alloc屬性賦值    sdssetalloc(s, newlen);    return s;}

        總結(jié)

        1.SDS 返回的是指向 buf 的指針,兼容了 C 語(yǔ)言操作字符串的函數(shù),讀取內(nèi)容時(shí),通過(guò) len 屬性來(lái)限制讀取的長(zhǎng)度,不受?\0?影響,從而保證二進(jìn)制安全;2.Redis 根據(jù)字符串長(zhǎng)度的不同,定義了多種數(shù)據(jù)結(jié)構(gòu),包括:sdshdr5/sdshdr8/sdshdr16/sdshdr32/sdshdr64。3.SDS 在設(shè)計(jì)字符串修改出會(huì)調(diào)用?sdsMakeRoomFor?函數(shù)進(jìn)行檢查,根據(jù)不同情況進(jìn)行擴(kuò)容。

        全文完!

        希望本文對(duì)大家有所幫助。如果感覺(jué)本文有幫助,有勞轉(zhuǎn)發(fā)或點(diǎn)一下“在看”!讓更多人收獲知識(shí)!


        長(zhǎng)按識(shí)別下圖二維碼,關(guān)注公眾號(hào)「Doocs 開(kāi)源社區(qū)」,第一時(shí)間跟你們分享好玩、實(shí)用的技術(shù)文章與業(yè)內(nèi)最新資訊。



        瀏覽 17
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            蜜臀久久精品99国产精品日本 | 亚洲午夜电影网 | 日本搞逼视频 | 男女男网站 | 国产精品不卡顿 | 九九九免费视频 | 人人爽人人爽人人片av免 | 大香蕉第四色 | 久久精品国产亚洲 | 惩罚女仆扒开打屁股二次元 |