<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深挖 Redis 6.0 源碼—— SDS

          共 5001字,需瀏覽 11分鐘

           ·

          2020-08-21 06:17

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

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

          項目地址:https://github.com/doocs/source-code-hunter

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

          學習之前,首先我們要明確,Redis 是一個使用 C 語言編寫的鍵值對存儲系統(tǒng)

          前置思考

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

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

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

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

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

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

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

          這樣做有以下幾個好處:

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

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

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

          Redis SDS 最新實現(xiàn)

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

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

          Redis 增加了一個 flags 字段來標識類型,用一個字節(jié)(8 位)來存儲。

          其中:前 3 位表示字符串的類型;剩余 5 位,可以用來存儲長度小于 32 的短字符串。

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

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

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

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

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

          SDS API 實現(xiàn)

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

          sds sdsnewlen(const void *init, size_t initlen) {    void *sh;    sds s;    // 根據(jù)字符串長度計算相應類型    char type = sdsReqType(initlen);    // 如果創(chuàng)建的是""字符串,強轉(zhuǎn)為SDS_TYPE_8    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;    // 根據(jù)類型計算頭部所需長度(頭部包含 len、alloc、flags)    int hdrlen = sdsHdrSize(type);    // 指向flags的指針    unsigned char *fp;    // 創(chuàng)建字符串,+1是因為 `\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ù)字符串長度計算得到 type,根據(jù) type 計算頭部所需長度,然后動態(tài)分配內(nèi)存空間。

          注意:

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

          2. 清空字符串

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

          一種是通過 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]));}

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

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

          3. 拼接字符串

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

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

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

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

          sds sdscatlen(sds s, const void *t, size_t len) {    // 計算當前字符串長度    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 的擴容策略是這樣的:

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

          sds sdsMakeRoomFor(sds s, size_t addlen) {    void *sh, *newsh;    // 當前剩余長度    size_t avail = sdsavail(s);    size_t len, newlen;    char type, oldtype = s[-1] & SDS_TYPE_MASK;    int hdrlen;    /* 剩余長度>=新增字符串長度,直接返回 */    if (avail >= addlen) return s;    // 計算當前字符串長度len    len = sdslen(s);    sh = (char*)s-sdsHdrSize(oldtype);    // 計算新長度    newlen = (len+addlen);    // 新長度<1MB,按新長度的2倍擴容    if (newlen < SDS_MAX_PREALLOC)        newlen *= 2;    // 否則按新長度+1MB擴容    else        newlen += SDS_MAX_PREALLOC;    // 計算新長度所屬類型    type = sdsReqType(newlen);    /* type5不支持擴容,強轉(zhuǎn)為type8 */    if (type == SDS_TYPE_5) type = SDS_TYPE_8;    hdrlen = sdsHdrSize(type);    if (oldtype==type) {        // 類型沒變,直接通過realloc擴大動態(tài)數(shù)組即可。        newsh = s_realloc(sh, hdrlen+newlen+1);        if (newsh == NULL) return NULL;        s = (char*)newsh+hdrlen;    } else {        // 類型改變了,則說明頭部長度也發(fā)生了變化,不進行realloc操作,而是直接重新開辟內(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 語言操作字符串的函數(shù),讀取內(nèi)容時,通過 len 屬性來限制讀取的長度,不受?\0?影響,從而保證二進制安全;2.Redis 根據(jù)字符串長度的不同,定義了多種數(shù)據(jù)結(jié)構(gòu),包括:sdshdr5/sdshdr8/sdshdr16/sdshdr32/sdshdr64。3.SDS 在設計字符串修改出會調(diào)用?sdsMakeRoomFor?函數(shù)進行檢查,根據(jù)不同情況進行擴容。

          全文完!

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


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



          瀏覽 17
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产足交在线观看 | 欧美成在线观看 | 欧美日韩电影一区二区三区 | 天堂国产一区二区三区不卡 | 内射亚洲美女 |