本文是《Redis內部數據結構詳解》系列的第二篇,講述Redis中使用最多的一個基礎數據結構:sds。html
無論在哪門編程語言當中,字符串都幾乎是使用最多的數據結構。sds正是在Redis中被普遍使用的字符串結構,它的全稱是Simple Dynamic String。與其它語言環境中出現的字符串相比,它具備以下顯著的特色:git
- 可動態擴展內存。sds表示的字符串其內容能夠修改,也能夠追加。在不少語言中字符串會分爲mutable和immutable兩種,顯然sds屬於mutable類型的。
- 二進制安全(Binary Safe)。sds能存儲任意二進制數據,而不只僅是可打印字符。
- 與傳統的C語言字符串類型兼容。這個的含義接下來立刻會討論。
看到這裏,不少對Redis有所瞭解的同窗可能已經產生了一個疑問:Redis已經對外暴露了一個字符串結構,叫作string,那這裏所說的sds到底和string是什麼關係呢?可能有人會猜:string是基於sds實現的。這個猜測已經很是接近事實,但在描述上還不太準確。有關string和sds之間關係的詳細分析,咱們放在後面再講。如今爲了方便討論,讓咱們先暫時簡單地認爲,string的底層實現就是sds。github
在討論sds的具體實現以前,咱們先站在Redis使用者的角度,來觀察一下string所支持的一些主要操做。下面是一個操做示例:redis

以上這些操做都比較簡單,咱們簡單解釋一下:編程
- 初始的字符串的值設爲」tielei」。
- 第3步經過append命令對字符串進行了追加,變成了」tielei zhang」。
- 而後經過setbit命令將第53個bit設置成了1。bit的偏移量從左邊開始算,從0開始。其中第48~55bit是中間的空格那個字符,它的ASCII碼是0x20。將第53個bit設置成1以後,它的ASCII碼變成了0x24,打印出來就是’$’。所以,如今字符串的值變成了」tielei$zhang」。
- 最後經過getrange取從倒數第5個字節到倒數第1個字節的內容,獲得」zhang」。
這些命令的實現,有一部分是和sds的實現有關的。下面咱們開始詳細討論。數組
sds的數據結構定義
咱們知道,在C語言中,字符串是以’\0’字符結尾(NULL結束符)的字符數組來存儲的,一般表達爲字符指針的形式(char *)。它不容許字節0出如今字符串中間,所以,它不能用來存儲任意的二進制數據。安全
咱們能夠在sds.h中找到sds的類型定義:數據結構
確定有人感到困惑了,居然sds就等同於char ?咱們前面提到過,sds和傳統的C語言字符串保持類型兼容,所以它們的類型定義是同樣的,都是char 。在有些狀況下,須要傳入一個C語言字符串的地方,也確實能夠傳入一個sds。可是,sds和char *並不等同。sds是Binary Safe的,它能夠存儲任意二進制數據,不能像C語言字符串那樣以字符’\0’來標識字符串的結束,所以它必然有個長度字段。但這個長度字段在哪裏呢?實際上sds還包含一個header結構:app
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* used */ uint8_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 { uint16_t len; /* used */ uint16_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 { uint32_t len; /* used */ uint32_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* used */ uint64_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; };
|
sds一共有5種類型的header。之因此有5種,是爲了能讓不一樣長度的字符串可使用不一樣大小的header。這樣,短字符串就能使用較小的header,從而節省內存。curl
一個sds字符串的完整結構,由在內存地址上先後相鄰的兩部分組成:
- 一個header。一般包含字符串的長度(len)、最大容量(alloc)和flags。sdshdr5有所不一樣。
- 一個字符數組。這個字符數組的長度等於最大容量+1。真正有效的字符串數據,其長度一般小於最大容量。在真正的字符串數據以後,是空餘未用的字節(通常以字節0填充),容許在不從新分配內存的前提下讓字符串數據向後作有限的擴展。在真正的字符串數據以後,還有一個NULL結束符,即ASCII碼爲0的’\0’字符。這是爲了和傳統C字符串兼容。之因此字符數組的長度比最大容量多1個字節,就是爲了在字符串長度達到最大容量時仍然有1個字節存放NULL結束符。
除了sdshdr5以外,其它4個header的結構都包含3個字段:
- len: 表示字符串的真正長度(不包含NULL結束符在內)。
- alloc: 表示字符串的最大容量(不包含最後多餘的那個字節)。
- flags: 老是佔用一個字節。其中的最低3個bit用來表示header的類型。header的類型共有5種,在sds.h中有常量定義。
1 2 3 4 5
|
#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
|
sds的數據結構,咱們有必要很是仔細地去解析它。

上圖是sds的一個內部結構的例子。圖中展現了兩個sds字符串s1和s2的內存結構,一個使用sdshdr8類型的header,另外一個使用sdshdr16類型的header。但它們都表達了一樣的一個長度爲6的字符串的值:」tielei」。下面咱們結合代碼,來解釋每一部分的組成。
sds的字符指針(s1和s2)就是指向真正的數據(字符數組)開始的位置,而header位於內存地址較低的方向。在sds.h中有一些跟解析header有關的宏定義:
1 2 3 4 5
|
#define SDS_TYPE_MASK 7 #define SDS_TYPE_BITS 3 #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)))) #define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
|
其中SDS_HDR用來從sds字符串得到header起始位置的指針,好比SDS_HDR(8, s1)表示s1的header指針,SDS_HDR(16, s2)表示s2的header指針。
固然,使用SDS_HDR以前咱們必須先知道究竟是哪種header,這樣咱們才知道SDS_HDR第1個參數應該傳什麼。由sds字符指針得到header類型的方法是,先向低地址方向偏移1個字節的位置,獲得flags字段。好比,s1[-1]和s2[-1]分別得到了s1和s2的flags的值。而後取flags的最低3個bit獲得header的類型。
- 因爲s1[-1] == 0x01 == SDS_TYPE_8,所以s1的header類型是sdshdr8。
- 因爲s2[-1] == 0x02 == SDS_TYPE_16,所以s2的header類型是sdshdr16。
有了header指針,就能很快定位到它的len和alloc字段:
- s1的header中,len的值爲0x06,表示字符串數據長度爲6;alloc的值爲0x80,表示字符數組最大容量爲128。
- s2的header中,len的值爲0x0006,表示字符串數據長度爲6;alloc的值爲0x03E8,表示字符數組最大容量爲1000。(注意:圖中是按小端地址構成)
在各個header的類型定義中,還有幾個須要咱們注意的地方:
- 在各個header的定義中使用了attribute ((packed)),是爲了讓編譯器以緊湊模式來分配內存。若是沒有這個屬性,編譯器可能會爲struct的字段作優化對齊,在其中填充空字節。那樣的話,就不能保證header和sds的數據部分牢牢先後相鄰,也不能按照固定向低地址方向偏移1個字節的方式來獲取flags字段了。
- 在各個header的定義中最後有一個char buf[]。咱們注意到這是一個沒有指明長度的字符數組,這是C語言中定義字符數組的一種特殊寫法,稱爲柔性數組(flexible array member),只能定義在一個結構體的最後一個字段上。它在這裏只是起到一個標記的做用,表示在flags字段後面就是一個字符數組,或者說,它指明瞭緊跟在flags字段後面的這個字符數組在結構體中的偏移位置。而程序在爲header分配的內存的時候,它並不佔用內存空間。若是計算sizeof(struct sdshdr16)的值,那麼結果是5個字節,其中沒有buf字段。
- sdshdr5與其它幾個header結構不一樣,它不包含alloc字段,而長度使用flags的高5位來存儲。所以,它不能爲字符串分配空餘空間。若是字符串須要動態增加,那麼它就必然要從新分配內存才行。因此說,這種類型的sds字符串更適合存儲靜態的短字符串(長度小於32)。
至此,咱們很是清楚地看到了:sds字符串的header,其實隱藏在真正的字符串數據的前面(低地址方向)。這樣的一個定義,有以下幾個好處:
- header和數據相鄰,而不用分紅兩塊內存空間來單獨分配。這有利於減小內存碎片,提升存儲效率(memory efficiency)。
- 雖然header有多個類型,但sds能夠用統一的char *來表達。且它與傳統的C語言字符串保持類型兼容。若是一個sds裏面存儲的是可打印字符串,那麼咱們能夠直接把它傳給C函數,好比使用strcmp比較字符串大小,或者使用printf進行打印。
弄清了sds的數據結構,它的具體操做函數就比較好理解了。
sds的一些基礎函數
- sdslen(const sds s): 獲取sds字符串長度。
- sdssetlen(sds s, size_t newlen): 設置sds字符串長度。
- sdsinclen(sds s, size_t inc): 增長sds字符串長度。
- sdsalloc(const sds s): 獲取sds字符串容量。
- sdssetalloc(sds s, size_t newlen): 設置sds字符串容量。
- sdsavail(const sds s): 獲取sds字符串空餘空間(即alloc - len)。
- sdsHdrSize(char type): 根據header類型獲得header大小。
- sdsReqType(size_t string_size): 根據字符串數據長度計算所須要的header類型。
這裏咱們挑選sdslen和sdsReqType的代碼,察看一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
static inline size_t sdslen(const sds s) { unsigned char flags = s[-1]; switch(flags&SDS_TYPE_MASK) { case SDS_TYPE_5: return SDS_TYPE_5_LEN(flags); case SDS_TYPE_8: return SDS_HDR(8,s)->len; case SDS_TYPE_16: return SDS_HDR(16,s)->len; case SDS_TYPE_32: return SDS_HDR(32,s)->len; case SDS_TYPE_64: return SDS_HDR(64,s)->len; } return 0; }
static inline char sdsReqType(size_t string_size) { if (string_size < 1<<5) return SDS_TYPE_5; if (string_size < 1<<8) return SDS_TYPE_8; if (string_size < 1<<16) return SDS_TYPE_16; if (string_size < 1ll<<32) return SDS_TYPE_32; return SDS_TYPE_64; }
|
跟前面的分析相似,sdslen先用s[-1]向低地址方向偏移1個字節,獲得flags;而後與SDS_TYPE_MASK進行按位與,獲得header類型;而後根據不一樣的header類型,調用SDS_HDR獲得header起始指針,進而得到len字段。
經過sdsReqType的代碼,很容易看到:
- 長度在0和2^5-1之間,選用SDS_TYPE_5類型的header。
- 長度在2^5和2^8-1之間,選用SDS_TYPE_8類型的header。
- 長度在2^8和2^16-1之間,選用SDS_TYPE_16類型的header。
- 長度在2^16和2^32-1之間,選用SDS_TYPE_32類型的header。
- 長度大於2^32的,選用SDS_TYPE_64類型的header。能表示的最大長度爲2^64-1。
注:sdsReqType的實現代碼,直到3.2.0,它在長度邊界值上都一直存在問題,直到最近3.2 branch上的commit 6032340才修復。
sds的建立和銷燬
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
sds sdsnewlen(const void *init, size_t initlen) { void *sh; sds s; char type = sdsReqType(initlen); /* Empty strings are usually created in order to append. Use type 8 * since type 5 is not good at this. */ if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; int hdrlen = sdsHdrSize(type); unsigned char *fp; /* flags pointer. */
sh = s_malloc(hdrlen+initlen+1); if (!init) memset(sh, 0, hdrlen+initlen+1); if (sh == NULL) return NULL; s = (char*)sh+hdrlen; fp = ((unsigned char*)s)-1; switch(type) { case SDS_TYPE_5: { *fp = type | (initlen << SDS_TYPE_BITS); break; } case SDS_TYPE_8: { SDS_HDR_VAR(8,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } case SDS_TYPE_16: { SDS_HDR_VAR(16,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } case SDS_TYPE_32: { SDS_HDR_VAR(32,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } case SDS_TYPE_64: { SDS_HDR_VAR(64,s); sh->len = initlen; sh->alloc = initlen; *fp = type; break; } } if (initlen && init) memcpy(s, init, initlen); s[initlen] = '\0'; return s; }
sds sdsempty(void) { return sdsnewlen("",0); }
sds sdsnew(const char *init) { size_t initlen = (init == NULL) ? 0 : strlen(init); return sdsnewlen(init, initlen); }
void sdsfree(sds s) { if (s == NULL) return; s_free((char*)s-sdsHdrSize(s[-1])); }
|
sdsnewlen建立一個長度爲initlen的sds字符串,並使用init指向的字符數組(任意二進制數據)來初始化數據。若是init爲NULL,那麼使用全0來初始化數據。它的實現中,咱們須要注意的是:
- 若是要建立一個長度爲0的空字符串,那麼不使用SDS_TYPE_5類型的header,而是轉而使用SDS_TYPE_8類型的header。這是由於建立的空字符串通常接下來的操做極可能是追加數據,但SDS_TYPE_5類型的sds字符串不適合追加數據(會引起內存從新分配)。
- 須要的內存空間一次性進行分配,其中包含三部分:header、數據、最後的多餘字節(hdrlen+initlen+1)。
- 初始化的sds字符串數據最後會追加一個NULL結束符(s[initlen] = ‘\0’)。
關於sdsfree,須要注意的是:內存要總體釋放,因此要先計算出header起始指針,把它傳給s_free函數。這個指針也正是在sdsnewlen中調用s_malloc返回的那個地址。
sds的鏈接(追加)操做
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
sds sdscatlen(sds s, const void *t, size_t len) { size_t curlen = sdslen(s);
s = sdsMakeRoomFor(s,len); if (s == NULL) return NULL; memcpy(s+curlen, t, len); sdssetlen(s, curlen+len); s[curlen+len] = '\0'; return s; }
sds sdscat(sds s, const char *t) { return sdscatlen(s, t, strlen(t)); }
sds sdscatsds(sds s, const sds t) { return sdscatlen(s, t, sdslen(t)); }
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;
/* Return ASAP if there is enough space left. */ if (avail >= addlen) return s;
len = sdslen(s); sh = (char*)s-sdsHdrSize(oldtype); newlen = (len+addlen); if (newlen < SDS_MAX_PREALLOC) newlen *= 2; else newlen += SDS_MAX_PREALLOC;
type = sdsReqType(newlen);
/* Don't use type 5: the user is appending to the string and type 5 is * not able to remember empty space, so sdsMakeRoomFor() must be called * at every appending operation. */ if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type); if (oldtype==type) { newsh = s_realloc(sh, hdrlen+newlen+1); if (newsh == NULL) return NULL; s = (char*)newsh+hdrlen; } else { /* Since the header size changes, need to move the string forward, * and can't use realloc */ newsh = s_malloc(hdrlen+newlen+1); if (newsh == NULL) return NULL; memcpy((char*)newsh+hdrlen, s, len+1); s_free(sh); s = (char*)newsh+hdrlen; s[-1] = type; sdssetlen(s, len); } sdssetalloc(s, newlen); return s; }
|
sdscatlen將t指向的長度爲len的任意二進制數據追加到sds字符串s的後面。本文開頭演示的string的append命令,內部就是調用sdscatlen來實現的。
在sdscatlen的實現中,先調用sdsMakeRoomFor來保證字符串s有足夠的空間來追加長度爲len的數據。sdsMakeRoomFor可能會分配新的內存,也可能不會。
sdsMakeRoomFor是sds實現中很重要的一個函數。關於它的實現代碼,咱們須要注意的是:
- 若是原來字符串中的空餘空間夠用(avail >= addlen),那麼它什麼也不作,直接返回。
- 若是須要分配空間,它會比實際請求的要多分配一些,以防備接下來繼續追加。它在字符串已經比較長的狀況下要至少多分配SDS_MAX_PREALLOC個字節,這個常量在sds.h中定義爲(1024*1024)=1MB。
- 按分配後的空間大小,可能須要更換header類型(原來header的alloc字段過短,表達不了增長後的容量)。
- 若是須要更換header,那麼整個字符串空間(包括header)都須要從新分配(s_malloc),並拷貝原來的數據到新的位置。
- 若是不須要更換header(原來的header夠用),那麼調用一個比較特殊的s_realloc,試圖在原來的地址上從新分配空間。s_realloc的具體實現得看Redis編譯的時候選用了哪一個allocator(在Linux上默認使用jemalloc)。但無論是哪一個realloc的實現,它所表達的含義基本是相同的:它儘可能在原來分配好的地址位置從新分配,若是原來的地址位置有足夠的空餘空間完成從新分配,那麼它返回的新地址與傳入的舊地址相同;不然,它分配新的地址塊,並進行數據搬遷。參見http://man.cx/realloc。
從sdscatlen的函數接口,咱們能夠看到一種使用模式:調用它的時候,傳入一箇舊的sds變量,而後它返回一個新的sds變量。因爲它的內部實現可能會形成地址變化,所以調用者在調用完以後,原來舊的變量就失效了,而都應該用新返回的變量來替換。不只僅是sdscatlen函數,sds中的其它函數(好比sdscpy、sdstrim、sdsjoin等),還有Redis中其它一些能自動擴展內存的數據結構(如ziplist),也都是一樣的使用模式。
淺談sds與string的關係
如今咱們回過頭來看看本文開頭給出的string操做的例子。
- append操做使用sds的sdscatlen來實現。前面已經提到。
- setbit和getrange都是先根據key取到整個sds字符串,而後再從字符串選取或修改指定的部分。因爲sds就是一個字符數組,因此對它的某一部分進行操做彷佛都比較簡單。
可是,string除了支持這些操做以外,當它存儲的值是個數字的時候,它還支持incr、decr等操做。那麼,當string存儲數字值的時候,它的內部存儲仍是sds嗎?實際上,不是了。並且,這種狀況下,setbit和getrange的實現也會有所不一樣。這些細節,咱們放在下一篇介紹robj的時候再進行系統地討論。
原文連接