redis內部資料結構之SDS簡單動態字串詳解
前言
reids 沒有直接使用C語言傳統的字串表示(以空字元結尾的字元陣列)而是構建了一種名為簡單動態字串的抽象型別,併為redis的預設字串表示,因為C字串不能滿足redis對字串的安全性、效率以及功能方面的需求
1、SDS 定義
在C語言中,字串是以'\0'字元結尾(NULL結束符)的字元陣列來儲存的,通常表達為字元指標的形式(char *)。它不允許位元組0出現在字串中間,因此,它不能用來儲存任意的二進位制資料。
sds的型別定義
typedef char *sds;
每個sds.h/sdshdr結構表示一個SDS的值 struct sdshdr{ //記錄buf陣列中已使用的位元組的數量 //等於sds所儲存字串的長度 int len; //記錄buf中未使用的資料 int free; //字元陣列,用於儲存字串 } * free 屬性的值為0,表示這個SDS沒有分配任何未使用的空間 * len 屬性長度為5,表示這個SDS儲存一個五位元組長的字串 * buf 屬性是一個char型別的陣列,陣列的前5個位元組分別儲存了'R','e','d','i','s'五個字元,而最後一個位元組則儲存了空字串'\0'
肯定有人感到困惑了,竟然sds就等同於char *?
sds和傳統的C語言字串保持型別相容,因此它們的型別定義是一樣的,都是char *,在有些情況下,需要傳入一個C語言字串的地方,也確實可以傳入一個sds。
但是sds和char *並不等同,sds是Binary Safe的,它可以儲存任意二進位制資料,不能像C語言字串那樣以字元'\0'來標識字串的結束,因此它必然有個長度欄位,這個欄位在header中
sds的header結構
/* Note: sdshdr5 is never used,we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. */ 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。目的是節省記憶體。
一個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的型別定義中,還有幾個需要我們注意的地方:
- 在各個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型別。
二、SDS 陣列動態分配策略
header資訊中的定義這麼多欄位,其中一個很重要的作用就是實現對字串的靈活操作並且儘量減少記憶體重新分配和回收操作。
redis的記憶體分配策略如下
- 當SDS的len屬性長度小於1MB時,redis會分配和len相同長度的free空間。至於為什麼這樣分配呢,上次用了len長度的空間,那麼下次程式可能也會用len長度的空間,所以redis就為你預分配這麼多的空間。
- 但是當SDS的len屬性長度大於1MB時,程式將多分配1M的未使用空間。這個時候我在根據這種慣性預測來分配的話就有點得不償失了。所以redis是將1MB設為一個風險值,沒過風險值你用多少我就給你多少,過了的話那這個風險值就是我能給你臨界值
reids的記憶體回收策略如下
- redis的記憶體回收採用惰性回收,即你把字串變短了,那麼多餘的記憶體空間我先不還給作業系統,先留著,萬一馬上又要被使用呢。短暫的持有資源,既可以充分利用資源,也可以不浪費資源。這是一種很優秀的思想。
綜上所述,redis實現的高效能字串的結果就把N次字串操作必會發生N次記憶體重新分配變為人品最差時最多發生N次重新分配。
/* Enlarge the free space at the end of the sds string so that the caller * is sure that after calling this function can overwrite up to addlen * bytes after the end of the string,plus one more byte for nul term. * * Note: this does not change the *length* of the sds string as returned * by sdslen(),but only the free buffer space we have. */ 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; } /* Reallocate the sds string so that it has no free space at the end. The * contained string remains not altered,but next concatenation operations * will require a reallocation. * * After the call,the passed sds string is no longer valid and all the * references must be substituted with the new pointer returned by the call. */ sds sdsRemoveFreeSpace(sds s) { void *sh,*newsh; char type,oldtype = s[-1] & SDS_TYPE_MASK; int hdrlen; size_t len = sdslen(s); sh = (char*)s-sdsHdrSize(oldtype); type = sdsReqType(len); hdrlen = sdsHdrSize(type); if (oldtype==type) { newsh = s_realloc(sh,hdrlen+len+1); if (newsh == NULL) return NULL; s = (char*)newsh+hdrlen; } else { newsh = s_malloc(hdrlen+len+1); if (newsh == NULL) return NULL; memcpy((char*)newsh+hdrlen,len); return s; }
三、SDS的特點
sds正是在Redis中被廣泛使用的字串結構,它的全稱是Simple Dynamic String。與其它語言環境中出現的字串相比,它具有如下顯著的特點:
- 可動態擴充套件記憶體。SDS表示的字串其內容可以修改,也可以追加。在很多語言中字串會分為mutable和immutable兩種,SDS屬於mutable型別的。
- 二進位制安全(Binary Safe)。sds能儲存任意二進位制資料。
- 與傳統的C語言字串型別相容。
- 預分配空間,可以懶惰釋放,在記憶體緊張的時候也可以縮減不需要的記憶體
- 常數複雜度獲取字串長度
- 杜絕緩衝區溢位,邊界檢查
四、淺談SDS與string的關係
127.0.0.1:6379> set test test OK 127.0.0.1:6379> append test " test" (integer) 9 127.0.0.1:6379> get test "test test" 127.0.0.1:6379> setbit test 36 1 (integer) 0 127.0.0.1:6379> get test "test(test" 127.0.0.1:6379> getrange test -5 -1 "(test"
- append操作使用SDS的sdscatlen來實現。
- setbit和getrange都是先根據key取到整個sds字串,然後再從字串選取或修改指定的部分。由於SDS就是一個字元陣列,所以對它的某一部分進行操作似乎都比較簡單。
但是,string除了支援這些操作之外,當它儲存的值是個數字的時候,它還支援incr、decr等操作。它的內部儲存不是SDS,這種情況下,setbit和getrange的實現也會有所不同。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對我們的支援。
參考文章
- http://blog.csdn.net/xiejingfa/article/details/50972592
- http://blog.csdn.net/acceptedxukai/article/details/17482611
- https://segmentfault.com/a/1190000003984537