0、引言
Redis沒有直接使用C語言傳統(tǒng)的字符串表示(以'\0'結(jié)尾的字符數(shù)組),而是構(gòu)建了一種名為簡單動態(tài)字符串(simple dynamic string, sds)的類型,用于Redis默認字符串的表示。
從2009年3月的1st commit到目前的最新版5.0.4,sds的變化相對是比較小的,中間只有一次較大的改動。commit記錄(PR#2509)如下:
主要是優(yōu)化了內(nèi)存使用、提高了字符串最大長度。該修改最早出現(xiàn)在3.2.0 RC1(Redis 3.2.0 RC1 (version 3.1.101) )Release date: 23 dec 2015):
* [NEW] SDS improvements for speed and maximum string length.
This makes Redis more memory efficient in different use cases.
(Design and implementation by Oran Agra, some additional work
by Salvatore Sanfilippo)
本文嘗試從源碼角度解析Redis SDS的設(shè)計、實現(xiàn)和演化。
1、SDS的定義
這里的定義是PR#2509之前的定義:
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};
len:記錄buf數(shù)組中已使用字節(jié)的數(shù)量,不計'\0'
free:記錄buf數(shù)組中未使用字節(jié)的數(shù)量,不計'\0'
buf:字節(jié)數(shù)組,用于保存字符串。
SDS遵循C字符串以'\0'結(jié)尾的慣例,這樣做的好處是:SDS可以直接重用一部分C字符串函數(shù)庫里面的函數(shù)。如:
printf("%s", s->buf);
2、SDS與C字符串的區(qū)別
SDS相比C字符串,在安全性、性能、功能性具有優(yōu)勢:
1、安全性:防止緩沖區(qū)溢出、二進制安全
2、性能:獲取字符串長度、空間預(yù)分配、惰性釋放
3、功能性:二進制安全、兼容部分C字符串函數(shù)
2.1、緩沖區(qū)溢出
緩沖區(qū)溢出(buffer overflow):是這樣的一種異常,當(dāng)程序?qū)?shù)據(jù)寫入緩沖區(qū)時,會超過緩沖區(qū)的邊界,并覆蓋相鄰的內(nèi)存位置。
C字符串不記錄自身長度,不會自動進行邊界檢測,增加了溢出風(fēng)險。如下面這種情形:
char* strcat(char* dest, const char* src);
s1 = 'Redis',s2 = 'MongoDB',當(dāng)執(zhí)行strcat(s1, " Cluster")時,未給s1分配足夠內(nèi)存空間,s1的數(shù)據(jù)將溢出到s2所在的內(nèi)存空間,導(dǎo)致s2保存的內(nèi)容被意外地修改。
而SDS記錄了自身長度,同時在修改時,API會按照如下步驟進行:
1)先檢查SDS的空間是否滿足修改所需的要求;
2)如果不滿足要求的話,API會自動將SDS的空間擴展至執(zhí)行修改所需的大小(realloc);
3)然后才執(zhí)行實際的修改操作。
例子可見:sds.c/sdscat
2.2、二進制安全
二進制安全(binary-safe):指能處理任意的二進制數(shù)據(jù),包括非ASCII和null字節(jié)。
C字符串中的字符必須符合某種編碼(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否則最先被程序讀入的空字符會被誤認為是字符串的結(jié)尾。這些限制使得C字符串只能保存文本數(shù)據(jù),不能保存圖像、視頻、音頻等二進制數(shù)據(jù)。
而SDS不會對數(shù)據(jù)做任何限制、過濾、假設(shè),數(shù)據(jù)在寫入時是什么樣子的,它被讀取時就是什么樣的。因此Redis不僅可以保存文本數(shù)據(jù),還可以保存任意格式的二進制數(shù)據(jù)。
2.3、獲取字符串長度
C字符串并不記錄自身的長度信息,因此為了獲取一個C字符串的長度,必須遍歷整個字符串,直至'\0',其復(fù)雜度時O(n)。
而SDS記錄了自身長度len,因此通過O(1)復(fù)雜度就能獲取字符串的長度。
2.4、空間預(yù)分配
當(dāng)SDS的API對一個SDS進行修改,并且需要對SDS進行空間擴展的時候,程序不僅會為SDS分配修改所必須的空間,還會為SDS分配額外的未使用空間。具體策略見sds.c/sdsMakeRoomFor
/* 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) {
struct sdshdr *sh, *newsh;
size_t free = sdsavail(s);
size_t len, newlen;
if (free >= addlen) return s;
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
if (newsh == NULL) return NULL;
newsh->free = newlen - len;
return newsh->buf;
}
#define SDS_MAX_PREALLOC (1024*1024)
2.5、惰性釋放
當(dāng)SDS的API需要縮短SDS保存的字符串時,程序并不立即使用內(nèi)存重分配來回收縮短后多出來的字節(jié),而是使用free屬性將這些字節(jié)的數(shù)量記錄起來,并等待將來使用。
下面這個api是將sds置空,但并未真正釋放內(nèi)存:
/* 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) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
sh->free += sh->len;
sh->len = 0;
sh->buf[0] = '\0';
}
真正釋放sds內(nèi)存的api:
/* Free an sds string. No operation is performed if 's' is NULL. */
void sdsfree(sds s) {
if (s == NULL) return;
zfree(s-sizeof(struct sdshdr));
}
回收free內(nèi)存的api:
/* 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) {
struct sdshdr *sh;
sh = (void*) (s-(sizeof(struct sdshdr)));
sh = zrealloc(sh, sizeof(struct sdshdr)+sh->len+1);
sh->free = 0;
return sh->buf;
}
2.6、兼容部分C字符串函數(shù)
SDS的buf數(shù)組會以'\0'結(jié)尾,這樣SDS就可以重用C字符串函數(shù)庫里的一些函數(shù),避免了不必要的代碼重復(fù)。
3、SDS的演化
這里主要介紹的是PR#2509和緊接著的antirez的“New sds type 5 implemented”。
sds的定義變?yōu)椋?/p>
/* 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[];
};
首先,sdshdr結(jié)構(gòu)體由1個變成了5個。__attribute__ ((__packed__))
的作用就是告訴編譯器取消結(jié)構(gòu)體在編譯過程中的優(yōu)化對齊,按照實際占用字節(jié)數(shù)進行對齊。
在解釋新的SDS結(jié)構(gòu)為何能優(yōu)化內(nèi)存前,先看一個測試:
#include <iostream>
#include <cstdint>
using namespace std;
struct sdshdr
{
unsigned int len;
unsigned int free;
char buf[];
};
struct sdshdr2
{
unsigned int len;
unsigned int free;
};
struct sdshdr3
{
char buf[];
};
int main() {
cout << sizeof(struct sdshdr) << endl; // 8
cout << sizeof(unsigned int) << endl << endl; // 4
cout << sizeof(uint8_t) << endl; // 1
cout << sizeof(uint16_t) << endl; // 2
cout << sizeof(uint32_t) << endl; // 4
cout << sizeof(uint64_t) << endl << endl; // 8
cout << sizeof(struct sdshdr2) << endl; // 8
cout << sizeof(struct sdshdr3) << endl; // 1
cout << sizeof(char) << endl; // 1
return 0;
}
typedef unsigned int uint32_t;
為什么要如此優(yōu)化,或者說之前的sds結(jié)構(gòu)存在什么問題呢?
之前sds存在的問題:
1)浪費內(nèi)存。共8 bytes header,即使很短的字符串也需要4字節(jié)的len字段
2)字符串大小上限4G。len最大值:2^32 - 1,字符串大小上限:2^32 / 1024 / 1024 / 1024 = 4G
PR#2509的改進:
sdshdr8 3 bytes header
sdshdr16 5 bytes header
sdshdr32 9 bytes header
sdshdr64 17 bytes header
在antirez的“New sds type 5 implemented”提交中,有2點修改:
1)修改了flags
char flags; /* 2 lsb of type, and 6 msb of refcount */
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
oranagra的PR用2位表示類型,antirez則認為,大多數(shù)SDS字符串從未調(diào)整大小,并且最小的它們不太可能調(diào)整大小,因此可以使用3位代替。oranagra設(shè)想其他幾位可以用作引用計數(shù),antirez則暫時放棄了這種考慮,暫時將這些bit預(yù)留,可能用于非常小的字符串的額外編碼。
2)增加了sdshdr5
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
用低3位表示類型,剩余5位表示sds字符串長度,那么可以表示長度為31(2^5-1)的字符串,這種情況下,最多才1個字節(jié)的header。如果需要對該字符串調(diào)整大小,可以直接升級到下一級(SDS_TYPE_8,3 bytes header)。
下面看新的sds結(jié)構(gòu)怎么用,以sdslen為例:
#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)
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;
}
SDS_TYPE_32,類型為3,二進制0011
SDS_TYPE_MASK,mask為7,二進制0111
0011
& 0111
————
0011
作者驗證了速度提升在大多數(shù)命令中都不是很明顯,除了具有依賴于大量sdscatlen()或sdcatfmt()的工作負載的命令,其中可以使用高達25%的性能。但最重要的是這次改進添加了很重要的一項功能:為Redis提供了超過4G字符串的能力。
本文的不足之處,未對修改前后內(nèi)存對齊的問題做研究分析,參考資料3中有相關(guān)討論。對于該內(nèi)容,是以后學(xué)習(xí)的一個方向。
參考資料:
3、sds size classes - memory optimization #2509(記錄了antirez和oranagra關(guān)于優(yōu)化的詳細對話細節(jié))
4、Redis設(shè)計與實現(xiàn) (黃建宏)