redis源碼閱讀之數據結構sds
本系列文章爲結合閱讀redis5.0源碼以及網上查閱相關資料整理,如有錯誤,歡迎交流指正(QQ:2824759538)
sds是simple dynamic string的縮寫,從命名上我們可以對它進行一個初步的認識,它是一個動態可擴展的字符串類型。在redis內部實現中,sds取代了C默認的字符串類型char*,在redis中,除了只會作爲字符串字面常量來用的一些無需對字符串進行修改的地方之外,其他所有的字符串都是通過使用sds類型來表示。
本文通過源碼對sds類型的定義、實現以及附帶的一些操作進行剖析,最後通過上面的分析總結sds字符串的優缺點。
1、sds源碼剖析
在redis工程文件中,sds的定義和實現相關的內容都在sds.h和sds.c文件中。
sds數據類型包括數據頭header和數據內容buf兩部分。
- 數據頭header中存儲了該字符串的長度len、最大容量alloc、header的類型flag以及空字符柔性數組
- 數據內容buf中存儲了字符串數據內容。
- header和buf在內存地址上是相鄰並連續的,這樣做的目的提高了對sds類型的存取效率以及減少了內存碎片的產生
其結構大概的示意圖如下:
1.1 sds類型定義
typedef char *sds;
從sds定義可以看出其底層類型其實就是char*,與C語言字符串保持兼容,所以在某種情況下,C標準庫中對char*進行操作的函數同樣也適用於sds。sds類型與C語言字符串一個最大的區別就是:
- C語言默認字符串是以字符’\0’結尾的字符數組來存儲的,字符串中間不允許有’\0’字符,因爲它以’\0’字符判斷該字符串是否已經到達結尾,若中間有該結束符,字符串將被截斷。這也是爲什麼C語言字符串不是二進制安全的原因。
- sds類型同樣是以字符’\0’結尾的字符數組來存儲的,但是它允許字符串中間有’\0’字符,因爲他不是通過’\0’判斷字符串結尾,而是在包頭中存儲了當前字符串的長度,通過該長度來判斷當前字符串的大小和是否讀到結尾。所以sds類型是二進制安全的。
1.2 sds header(handler)定義
爲了優化redis的內存佔用,適應不同長度的字符串在內存中的存儲,sds header根據字符串的長度定義了5種header,分別是sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。
/* 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[];
};
#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
#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)
1.2.1 包頭結構分析
從header定義可以看出,除了sdshdr5沒有len和alloc字段之外,其他header結構體中都包括以下幾個字段:
-
len : 字符串長度,(不包括header和NULL結束符)
-
alloc:當前sds類型的最大容量(不包括header和NULL結束符)
-
flag:sds的header類型(低3位表示當前header的類型,高5位對於sdshdr5表示字符串長度,其他類型高5位沒有意義),從上面宏定義,分別是0~4表示5中不同的header。
-
buf[]:柔性數組,只是一個標記作用,表示flag之後還有一個字符數組,該字符數組內存儲字符串的實際內容。
宏定義SDS_HDR_VAR與SDS_HDR不難理解,SDS_HDR(T,s)將帶有長度爲T的header的sds s,轉換爲其數據頭header的地址。SDS_HDR_VAR(T,s)與SDS_HDR相同,將該頭的地址複製到變量sh中。
對header的定義使用了兩個小技巧,分別是使用_attribute_ ((_packed_))取消編譯過程中的優化對齊和使用柔性數組來保證header與數據內容在內存上是連續的。如果想要具體瞭解,可以參考我的兩外兩篇博客:
1、C語言柔性數組:保證header與data在內存分佈上連續,提高sds類型的存取效率和減少了內存碎片的產生。https://blog.csdn.net/u014630623/article/details/100858910
2、pack與aligned的區別:消除了不同編譯器對內存對齊優化編譯的差異帶來的問題https://blog.csdn.net/u014630623/article/details/88929716
3、C語言宏定義相關:記錄了宏定義的一些特殊用法,https://blog.csdn.net/u014630623/article/details/88959591
1.2.2 sds類型存儲示意圖
sds類型數據內存存儲示意圖:(sdshdr5包頭類型數據除外)
sdshdr5包頭類型數據內存存儲示意圖:
字符串存儲內存示意圖:
如使用sds類型存儲字符串"test123",初始化該字符串類型lenth(“test123”) < (1<<8),故使用sdshdr8作爲sds header存儲,數據在內存中存儲的示意圖如下:
sds* test_sds=“test123”;
其中SDS_HDR爲該數據的包頭地址,test_sds[-1]爲flag,存儲了該sds變量的包頭類型,
1.3 sds重點函數分析
前面我們分析了sds類型的存儲設計,接着我們分析sds相關的一些操作函數,並挑選其中幾個重點的進行分析。分析方式主要是在源碼中添加相關注釋的方式進行分析。
1.3.1 重點函數分析
1.3.1.1 獲取sds字符串長度
static inline size_t sdslen(const sds s) {
// 首先取出sds header中的flag字段s[-1],從上述示意圖中可以理解知flag即爲sds的前一個字符。
unsigned char flags = s[-1];
// 對flag做掩碼,取出低3位,判斷header的類型
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
// 若header類型爲sdshdr5,字符串長度即爲flag高5位
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
// 若header類型爲sdshdr5之外的類型,取出header指針SDS_HDR(T,s),通過指針取出len
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;
}
1.3.1.2 根據字符串長度計算需要的header類型
/*
* 根據字符串長度計算需要的header類型
* 每個類型位小於1<<T,而不是小於(1<<T-1)的原因是alloc和len中都沒有包括結尾符
*/
static inline char sdsReqType(size_t string_size) {
// 字符串長度小於5個字節,使用sdshdr5
if (string_size < 1<<5)
return SDS_TYPE_5;
// 字符串類型小於8個字節,使用sdshdr8
if (string_size < 1<<8)
return SDS_TYPE_8;
if (string_size < 1<<16)
return SDS_TYPE_16;
// 若LONG_MAX==LLONG_MAX,說明系統架構爲64位,此時根據字符串的長度選擇使用sdshdr32或sdshdr64
#if (LONG_MAX == LLONG_MAX)
if (string_size < 1ll<<32)
return SDS_TYPE_32;
return SDS_TYPE_64;
// 否則32位架構,統一使用sdshdr32
#else
return SDS_TYPE_32;
#endif
}
1.3.1.2 sds字符串的創建與銷燬
/* 通過init和initlen初始化sds字符串
* 若init=NULL,則字符串的內容全部初始化爲空,即所有字節都是\0
* 若init=SDS_NOINIT,則字符串的內容不做初始化。
*
* sds字符串總是以\0結尾,但是允許字符串中間出現\0
*/
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
// 首先根據字符串的長度計算需要的header類型
char type = sdsReqType(initlen);
// 若計算出類型位SDS_TYPE_5並且初始化長度爲0,此時通常是需要存儲變長字符串,直接使用SDS_TYPE_8類型。
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
// 計算數據包頭大小
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
// 申請內存,header+datalen+1(最後一位爲結尾符\0)
sh = s_malloc(hdrlen+initlen+1);
// 若init==SDS_NOINIT,不做初始化
if (init==SDS_NOINIT)
init = NULL;
// 若init==NULL,將整片內存初始化爲NULL
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
// 分配內存爲空或失敗,直接返回NULL
if (sh == NULL) return NULL;
// 將申請的內存偏移header len大小得到sds內存地址
s = (char*)sh+hdrlen;
// sds向前偏移一位,即爲flag 指針
fp = ((unsigned char*)s)-1;
// 初始化header數據
switch(type) {
case SDS_TYPE_5:
// sdshdr5中flag高5位爲字符串長度,低3位爲類型header類型
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
// 初始化 header中的len,alloc以及type字段。
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;
}
}
// 若init不空,將init中的數據拷貝到sds字符串中,二進制安全。
if (initlen && init)
memcpy(s, init, initlen);
// 數據結尾添加\0
s[initlen] = '\0';
return s;
}
/*創建一個空字符串,字符串長度爲0*/
sds sdsempty(void) {
return sdsnewlen("",0);
}
/* 通過C字符串創建一個新的sds字符串 */
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
/* 從sds創建一個新的字符串,深拷貝 */
sds sdsdup(const sds s) {
return sdsnewlen(s, sdslen(s));
}
/* sds爲NULL時不做操作,其他情況計算出header指針,並銷燬包括header和data在內的整塊內存 */
void sdsfree(sds s) {
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-1]));
}
/*clear只是將字符串標記爲可用,並沒有釋放內存空間,重新複製字符串或者追加不需要*/
void sdsclear(sds s) {
sdssetlen(s, 0);
s[0] = '\0';
}
1.3.1.3 sds最大容量(alloc)擴充函數
// 該函數用於擴充sds字符串的最大可用空間,以適應sds字符串擴充長度爲addlen的字符串需要
// 注意:該函數只會擴充sds的最大容量,而不會改變sds的長度len
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
// 計算當前sds的可用長度,即alloc-len
size_t avail = sdsavail(s);
size_t len, newlen;
// 獲取sds當前的包頭類型old_type
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
// 若當前可用空間滿足新增長度addlen,則直接返回,不做任何操作
if (avail >= addlen) return s;
// 計算新的擴充的sds長度,此處使用了一個小技巧爲sds預先分配內存,
// 若新的字符串長度<SDS_MAX_PREALLOC,分配2*newlen的空間
// 否則分配newlen+SDS_MAX_PREALLOC長度的空間,
// 這樣的內存分配策略 既可以保證可預先分配足夠的空間,又可以避免不浪費過多的內存空間。
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// 重新計算新的內存空間需要使用的header類型
type = sdsReqType(newlen);
// 與sdsnew一致,儘量不使用sdshdr5,不利於擴充
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 {
// header類型改變,需要將就數據移動到新的內存中
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);
}
// 修改sds最大容量大小
sdssetalloc(s, newlen);
return s;
}
1.3.1.4 longlong轉sds
// longlong帶符號最長21位
#define SDS_LLSTR_SIZE 21
int sdsll2str(char *s, long long value) {
char *p, aux;
unsigned long long v;
size_t l;
// 對long long循環取餘,放到臨時字符串中
v = (value < 0) ? -value : value;
p = s;
do {
*p++ = '0'+(v%10);
v /= 10;
} while(v);
if (value < 0) *p++ = '-';
// 計算字符串的長度,並在末尾加\0
l = p-s;
*p = '\0';
// 反轉字符串
p--;
while(s < p) {
aux = *s;
*s = *p;
*p = aux;
s++;
p--;
}
return l;
}
// 首先將value轉爲char*,然後使用char*構造sds
sds sdsfromlonglong(long long value) {
char buf[SDS_LLSTR_SIZE];
int len = sdsll2str(buf,value);
return sdsnewlen(buf,len);
}
1.3.1 sds相關函數列表
函數原型 | 描述 |
---|---|
size_t sdslen(const sds s) | 獲取sds中字符串的長度,不包括header和結尾符 |
size_t sdsavail(const sds s) | 獲取sds可用空間大小,即alloc-len |
void sdssetlen(sds s, size_t newlen) | 設置sds字符串長度 |
void sdsinclen(sds s, size_t inc) | 增加字符串 |
size_t sdsalloc(const sds s) | 獲取sds最大容量 |
void sdssetalloc(sds s, size_t newlen) | 設置字符串最大容量 |
int sdsHdrSize(char type) | 獲取sds header大小 |
char sdsReqType(size_t string_size) | 根據字符串大小確定sds header類型 |
sds sdsnewlen(const void *init, size_t initlen) | 從init和initlen初始化sds字符串 |
sds sdsempty(void) | 創建空sds字符串 |
sds sdsnew(const char *init) | 利用C字符串創建sds |
sds sdsdup(const sds s) | 複製sds字符串,深拷貝 |
void sdsfree(sds s) | 釋放sds字符串,並釋放內存 |
void sdsupdatelen(sds s) | 更新字符串的長度 |
void sdsclear(sds s) | 將字符串標記爲可用,不釋放內存 |
sds sdsMakeRoomFor(sds s, size_t addlen) | 擴充sds字符串的內存 |
sds sdsRemoveFreeSpace(sds s) | 重新分配sds字符串,使其末尾沒有可用空間 |
size_t sdsAllocSize(sds s) | 返回sds類型整個包占的總大小,包括header,data,爲使用的空間和結尾符 |
void *sdsAllocPtr(sds s) | 返回sds包在內存中的首地址,即包頭 |
void sdsIncrLen(sds s, ssize_t incr) | 1、incr>0 增加sds的長度,根據incr減少字符串末尾左側的可用空間,在新的末尾處設置’\0’ 2、incr<0 縮短sds的長度,對sds字符串進行右修剪。 |
sds sdsgrowzero(sds s, size_t len) | 增長sds以使其具有指定的長度。 不屬於sds原始長度的字節將被設置爲零。如果指定的長度小於當前長度,則不執行任何操作。 |
sds sdscatlen(sds s, const void *t, size_t len) | 鏈接兩個字符串 |
sds sdscat(sds s, const char *t) | 連接sds與C字符串 |
sds sdscatsds(sds s, const sds t) | 連接兩個sdsd字符串 |
sds sdscpylen(sds s, const char *t, size_t len) | 複製字符串,將t複製到sds,內存不足則分配 |
sds sdscpy(sds s, const char *t) | 複製C字符串 |
int sdsll2str(char* s, long long value) | long long轉C字符串 |
int sdsull2str(char *s, unsigned long long v) | unsigned longlong轉C字符串 |
sds sdsfromlonglong(long long value) { | long long轉sds字符串 |
sds sdscatvprintf(sds s, const char *fmt, va_list ap) | 將sds與格式化字符串va_list連接 |
sds sdscatprintf(sds s, const char *fmt, …) | sds與可變參數連接 |
sds sdscatfmt(sds s, char const *fmt, …) | 自定義format函數( %s - C String %S - SDS string %i - signed int %I - 64 bit signed integer*(long long, int64_t) %u - unsigned int* %U - 64 bit unsigned integer (unsigned long long, uint64_t) %% - Verbatim “%” character.) |
sds sdstrim(sds s, const char *cset) | 字符串裁剪字符串,從sds中刪除cset字符串 |
void sdsrange(sds s, ssize_t start, ssize_t end) | 取制定範圍內的sds,會修改原有sds數據 |
void sdstolower(sds s) | 字母全部轉小寫 |
void sdstoupper(sds s) | 字母全部轉大寫 |
int sdscmp(const sds s1, const sds s2) | 比較兩個sds,與C比較類似 |
sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count) | 將字符串s以sep爲分隔符進行分割,返回sds數組 |
void sdsfreesplitres(sds *tokens, int count) | 釋放上述split函數的返回值中所有sds的內存 |
sds sdscatrepr(sds s, const char *p, size_t len) | 在sds後追加轉移字符串,所有非打印字符將被轉義,調用後原sds失效 |
int is_hex_digit(char c) | 判斷字符C是否爲十六進制數字 |
int hex_digit_to_int(char c) | 十六進制轉十進制數字 |
sds *sdssplitargs(const char *line, int *argc) | 解析命令行參數,並將返回值結果存入sds數組中返回 |
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen) | 替換sds中指定字符串爲目標字符串 |
sds sdsjoin(char **argv, int argc, char *sep) | 以特定分隔符連接c字符串存入sds |
sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen) | 與上述方法作用一致,分隔符不一定是C字符串 |
2、SDS優缺點分析
從上面源碼分析的過程中,其實也能看出sds字符串的一些優缺點,現在做一個總結。
優點:
- sds字符串header與body在內存空間上連續,提高了字符串的存取效率,較少內存碎片的產生。
- 獲取字符串長度只需要常數時間O(1)複雜度
- sds字符串可動態擴展,有效避免C傳統字符串緩衝區溢出問題
- 在內存分配策略上,sds以一定策略預先分配預留空間,降低內存分配頻率和避免了分配內存空間過大導致的內存浪費問題,具體策略參考sdsnewlen的實現
- sds字符串與C字符串兼容,操作char*的函數同樣可用於操作sds數據(操作二進制數據可能會出現字符串丟失情況)
- 二進制安全
缺點:
- 對sds字符串進行內存重新分配後可能會導致sds指針失效,這個時候需要更新所有引用到該指針的地方,否則會發生不可預知錯誤