成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

【Redis5源碼學習】2019-04-15 簡單動態(tài)字符串SDS

Vixb / 3575人閱讀

摘要:關于結構體內存對齊是什么,請參考源碼學習內存管理筆記。這說明在當前情況下,字符串結構中的柔性數組的起始位置并不受是否加關鍵字而影響,是緊跟在結構體后面的,所以節(jié)省內存這個說法并不成立。

baiyan

全部視頻:https://segmentfault.com/a/11...

今天我們正式進入redis5源碼的學習。redis是一個由C語言編寫、基于內存、單進程、可持久化的Key-Value型數據庫,解決了磁盤存取速度慢的問題,大幅提升了數據訪問速度,所以它常常被用作緩存。

那么為什么redis會如此之快呢?讓我們首先從內部存儲的數據結構的角度,一步一步揭開它神秘的面紗。

在redis的set、get等常用命令中,最嘗試用的就是字符串類型。在redis中,存儲字符串的數據類型,叫做簡單動態(tài)字符串(Simple Dynamic String),即SDS,它在redis中是如何實現的呢?

引入

回顧我們之前在PHP7源碼分析中講到的zend_string結構:

struct _zend_string {
    zend_refcounted_h gc;         /*引用計數,與垃圾回收相關,暫不展開*/
    zend_ulong        h;          /* 冗余的hash值,計算數組key的哈希值時避免重復計算*/
    size_t            len;        /* 存長度 */
    char              val[1];     /* 柔性數組,真正存放字符串值 */
};

之前在【PHP7源碼學習】2019-03-13 PHP字符串筆記文中提到,設計一個存儲字符串的結構,最重要的就是存儲其長度字符串本身的內容。至于為什么存儲長度,是為了解決二進制安全的問題,且能夠以常量復雜度訪問到字符串的長度,詳情可以到上述文章中查看。

SDS新老結構的對比

在redis3.2.x之前,SDS的存儲結構如下:

struct sdshdr {
    int len; //存長度
    int free; //存字符串內容的柔性數組的剩余空間
    char buf[]; //柔性數組,真正存放字符串值 
}; 

以“Redis”字符串為例,我們看一下它在舊版SDS結構中是如何存儲的:

free字段為0,代表buf字段沒有剩余存儲空間

len字段為5,代表字符串長度為5

buf字段存儲真正的字符串內容“Redis”

存儲字符串內容的柔性數組占用內存大小為6字節(jié),其余字段所占用8個字節(jié)(4+4+6 = 14字節(jié))

在新版本redis5中,為了進一步減少字符串存儲過程中的內存占用,劃分了5種適應不同字符串長度專用的存儲結構:

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; //低三位存儲類型,高5位存儲字符串長度,這種字符串存儲類型很少使用
    char buf[]; //存儲字符串內容的柔性數組
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; //字符串長度
    uint8_t alloc; //已分配的總空間
    unsigned char flags; //標識是哪種存儲類型
    char buf[]; //存儲字符串內容的柔性數組
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; //字符串長度
    uint16_t alloc;  //已分配的總空間
    unsigned char flags; //標識是哪種存儲類型
    char buf[]; //存儲字符串內容的柔性數組
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; //字符串長度
    uint32_t alloc;  //已分配的總空間
    unsigned char flags; //標識是哪種存儲類型
    char buf[]; //存儲字符串內容的柔性數組
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; //字符串長度
    uint64_t alloc;  //已分配的總空間
    unsigned char flags; //標識是哪種存儲類型
    char buf[]; //存儲字符串內容的柔性數組
};

我們可以看到,SDS的存儲結構由一種變成了五種,他們之間的不同就在于存儲字符串長度的len字段和存儲已分配字節(jié)數的alloc字段的類型,分別占用了1、2、4、8字節(jié)(不考慮sdshdr5類型),這決定了這種結構能夠最大存儲多長的字符串(2^8/2^16/2^32/2^64)。

我們注意,這些結構體中都帶有__attribute__ ((__packed__))關鍵字,它告訴編譯器不進行結構體的內存對齊。這個關鍵字我們下文會詳細講解。關于結構體內存對齊是什么,請參考【PHP7源碼學習】2019-03-08 PHP內存管理2筆記。

利用gdb查看SDS的存儲結構

接著說我們之前存儲“Redis”的例子,我們需要先對其進行gdb,觀察"Redis”字符串使用了哪種結構,gdb的步驟如下:

首先到官網下載源碼包,編譯

啟動一個終端,進入redis源碼的src目錄下,后臺啟動一個redis-server:

./redis-server &

然后查看當前redis的后臺進程的pid:

ps -aux |grep redis

記錄下這個pid,然后利用gdb -p命令調試該端口(如端口號是11430):

gdb -p 11430

接著在setCommand函數處打一個斷點,這個函數用來執(zhí)行set命令,然后使用c命令執(zhí)行到斷點處:

(gdb) b setCommand
(gdb) c

有了redis服務端,我們還要啟動一個redis客戶端,接下來啟動另一個終端(同樣在src目錄下),啟動客戶端:

./redis-cli

接著我們在redis客戶端中執(zhí)行set命令,我們設置了一個key為Redis,值為1的key-value對:

127.0.0.1:6379> set Redis 1

返回我們之前終端中的服務端,我們發(fā)現它停在了setCommand處:

接著一直n下去,直到setGenericCommand函數,s進去,就可以看到我們的key “Redis”了,它是一個rObj結構(我們暫時不看),里面的ptr就指向字符串結構的buf字段,我們強轉一下,能夠看到字符串內容“Redis”。

我們知道,無論是這五種結構中的哪一種,其前一位一定是flag字段,我們打印它的值,它的值為1。那么1是什么含義呢,它被用來標識是這五種字符串結構中的哪一種:

#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

它的值為1,代表是sdshdr8類型,我們可以畫出當前字符串的存儲結構圖:

我們可以看到,它總共占用3+6 = 9字節(jié),比之前的14字節(jié)節(jié)省了5字節(jié)。通過對之前長度和alloc字段的細化(由之前的int轉為int8、int16、int32、int64),這樣一來,就會大大節(jié)省redis存儲字符串所占用的內存空間。內存空間是非常寶貴的,而且redis中最常用的數據類型就是字符串類型。雖然看起來節(jié)省的空間很少,但由于它非常常用,所以這樣做的好處是無窮大的。

關鍵字__attribute__ ((packed))的作用

該關鍵字用來告知編譯器不需要進行結構體的內存對齊。

為了測試__attribute__ ((packed))關鍵字在redis字符串結構中的作用,我們寫如下一段測試代碼:

  #include "stdio.h"
 
  int main(){
      struct __attribute__ ((__packed__))  sdshdr64{
          long long len;
          long long alloc;
          unsigned char flags;
          char buf[];
      };
 
      struct sdshdr64 s;
      s.len = 1;
      s.alloc = 2;
      printf("sizeof sds64 is %d", sizeof(s));
      return 1;
  }

我們定義一個結構體,其字段和redis中的字符串結構基本一致。如果加上__attribute__ ((__packed__)) ,應該不是內存對齊的。如果去掉它,就應該是內存對齊的,會比前一種情況更加浪費內存,所以會對齊會節(jié)省內存。我們現在猜想的內存結構圖應該如下所示:

我們首先驗證加上__attribute__ ((__packed__)) 的情況,我們預期應該是不對齊的,在gdb中內存地址如下:

我們看到,buf確實是從0x171地址處開始的,并沒有對齊。那么我們看另一種情況,去掉__attribute__ ((__packed__)),再進行gdb調試:

大家看這張圖,是不是和上一張圖一摸一樣(我真的去掉了并且重新編譯了?。。。?。這說明在當前情況下,redis字符串結構中的柔性數組的起始位置并不受是否加__attribute__ ((__packed__))關鍵字而影響,是緊跟在結構體后面的,所以節(jié)省內存這個說法并不成立。(不一定是所有情況下柔性數組都緊跟在結構體后面,如果把buf的類型改為int就不是緊跟在后面,大家感興趣可以自己調試一下)。

那么,為什么這里要加上__attribute__ ((__packed__)呢?我們換個思路,既然不能節(jié)省空間,那么能不能節(jié)省時間呢?會不會操作非對齊的結構體性能更好、效率更高,或者是寫代碼更方便、可閱讀性強呢?

筆者在這里的猜想是比較方便工程中的代碼編寫,可閱讀性更強,我的參考如下:

在sizeof運算符中,它返回的是結構體占用空間的大小,和是否對齊有很大關系。比如上例中的結構體,如果不加上__attribute__ ((__packed__)),說明需要內存對齊,sizeof(struct s)的返回結果應該為24(8+8+8);如果加上__attribute__ ((__packed__)),說明不需要對齊,返回的結果應該為17(8+8+1),我們打印一下:

結果和我們預期的一致。我們知道,在之前我們gdb的時候,rObj的指針直接指向柔性數組buf的地址,即字符串內容的起始地址。那么如何知道它的len和alloc的值呢?只需要用buf的地址ptr - sizeof(struct s)即可。在這里,如果加上__attribute__ ((__packed__)),它返回的結果是17,那么直接做減法,就可以到結構體開頭的位置,即可直接讀取len的值。如果不加__attribute__ ((__packed__)),它返回的結果是24,做減法就會的到錯誤的位置,這就是原因所在,在源碼中我們也可以看到,它確實是這么找到當前字符串結構體的頭部的:

#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))))

那么我們可能會問了,你剛才不是還用buf[-1]也能訪問到嗎?或者buf[-17],應該也能訪問到len吧。這里筆者簡單猜想可能是上一種寫法,在工程的代碼實現中,更加易讀也更加方便。更加深層的原因仍待討論。

為什么需要alloc字段

在之前的講解中,我們一直沒有提到alloc字段的作用。我們知道,它是目前給存儲字符串的柔性數組總共分配了多少字節(jié)的空間。那么記錄這個字段的作用何在呢?那就是空間預分配惰性空間釋放的設計思想了。

空間預分配:在需要對 SDS 進行空間擴展的時候, 程序不僅會為 SDS 分配修改所必須要的空間, 還會為 SDS 分配額外的未使用空間。舉一個例子,我們將字符串“Redis”擴展到“Redis111”,應用程序并不僅僅分配3個字節(jié),僅僅讓它恰好滿足分配的長度,而是會額外分配一些空間。具體如何分配,見下述代碼注釋。我們講其中一種分配方式,假設它會分配8字節(jié)的內存空間。現在總共的內存空間為5+8 = 13,而我們只用了前8個內存空間,還剩下5個內存空間未使用。那么我們?yōu)槭裁匆@樣做呢?這是因為如果我們再繼續(xù)對它進行擴展,如改成“Redis11111”,在擴展 SDS 空間之前,SDS API 會先檢查未使用空間是否足夠,如果足夠的話,API 就會直接使用未使用空間那么我們就不用再進行系統(tǒng)調用申請一次空間了,直接把追加的“11”放到之前分配過的空間處即可。這樣一來,會大大減少使用內存分配系統(tǒng)調用的次數,提高了性能與效率??臻g預分配的代碼如下:

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 = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC) //SDS_MAX_PREALLOC = 1MB,如果擴容后的長度小于1MB,直接額外分配擴容后字符串長度*2的空間
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC; //擴容后長度大于等于1MB,額外分配擴容后字符串+1MB的空間

     ...
     真正的去分配空間
     ...
     sdssetalloc(s, newlen);
     return s;
}

上述sdsavail函數在獲取字符串剩余可用空間的時候,就會使用到alloc字段。它記錄了分配的總空間大小,方便我們在進行字符串追加操作的時候,判斷是否需要額外分配空間。當前剩余的可用空間大小為alloc - len,即已分配總空間大小alloc - 當前使用的空間大小len

static inline size_t sdsavail(const sds s) {
    unsigned char flags = s[-1];
    switch(flags&SDS_TYPE_MASK) {
        case SDS_TYPE_5: {
            return 0;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_16: {
            SDS_HDR_VAR(16,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_32: {
            SDS_HDR_VAR(32,s);
            return sh->alloc - sh->len;
        }
        case SDS_TYPE_64: {
            SDS_HDR_VAR(64,s);
            return sh->alloc - sh->len;
        }
    }
    return 0;
}

惰性空間釋放:惰性空間釋放用于優(yōu)化 SDS 的字符串截取或縮短操作。當 SDS 的 API 需要縮短 SDS 保存的字符串時,程序并不立即回收縮短后多出來的字節(jié)。這樣一來,如果將來要對 SDS 進行增長操作的話,這些未使用空間就可能會派上用場。比如我們將“Redis111”縮短為“Redis”,然后又改成“Redis111”,這樣,如果我們立刻回收縮短后多出來的字節(jié),然后再重新分配內存空間,是非常浪費時間的。如果等待一段時間之后再回收,可以很好地避免了縮短字符串時所需的內存重分配操作, 并為將來可能有的增長操作提供了擴展空間。源碼中一個清空字符串的SDS API如下:

/* 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) {
    sdssetlen(s, 0);
    s[0] = "