簡(jiǎn)介
在redis內(nèi)部,string數(shù)據(jù)結(jié)構(gòu)的value主要以int、sds作為結(jié)構(gòu)存儲(chǔ)。int用來(lái)存放整型,sds用來(lái)存放字節(jié)、字符串、浮點(diǎn)型數(shù)據(jù)。
sds結(jié)構(gòu)分析
/* 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種類(lèi)型的header(sdshdr5 不再被使用了)。使得不同長(zhǎng)度的字符串可以使用不同大小的header,從而節(jié)省內(nèi)存。
接下來(lái)我們定義一個(gè)通用的sds結(jié)構(gòu),如下:
struct sdshdr {
XXX len; /* used */
XXX alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
- len:表示字符串的真正長(zhǎng)度(不包含NULL結(jié)束符);
- alloc:表示字符串的最大容量(不包含頭部和NULL結(jié)束符);
- flags:占用一個(gè)字節(jié),其中的最低3個(gè)bit用來(lái)表示header的類(lèi)型,其余5個(gè)bit未使用。對(duì)應(yīng)類(lèi)型如下:
#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
- buf:字符數(shù)組,存儲(chǔ)實(shí)際內(nèi)容。SDS 遵循 C 字符串以空字符結(jié)尾的慣例, 保存NULL字符的 1 字節(jié)空間不計(jì)算在SDS的len屬性、alloc屬性里面, 并且為空字符分配額外的 1 字節(jié)空間。
sds優(yōu)勢(shì)
C 語(yǔ)言使用長(zhǎng)度為 N+1 的字符數(shù)組來(lái)表示長(zhǎng)度為 N 的字符串, 并且字符數(shù)組的最后一個(gè)元素總是NULL字符 '\0' 。這種簡(jiǎn)單的字符串表示方式, 并不能滿(mǎn)足 Redis 對(duì)字符串在安全性、效率、以及功能方面的要求。
常數(shù)復(fù)雜度獲取字符串長(zhǎng)度
C 字符串并不記錄自身的長(zhǎng)度信息, 所以為了獲取一個(gè) C 字符串的長(zhǎng)度, 程序必須遍歷整個(gè)字符串, 對(duì)遇到的每個(gè)字符進(jìn)行計(jì)數(shù), 直到遇到代表字符串結(jié)尾的空字符為止, 這個(gè)操作的復(fù)雜度為 O(N);
sds記錄字符串長(zhǎng)度,可以直接返回len,復(fù)雜度為 O(1);
杜絕緩沖區(qū)溢出
C 字符串不記錄自身長(zhǎng)度帶來(lái)的另一個(gè)問(wèn)題是容易造成緩沖區(qū)溢出。
如上圖所示,str1="hello",str2="world",str1與str2存儲(chǔ)在連續(xù)的內(nèi)存空間,接著對(duì)str1拼接“&hello”(c語(yǔ)言提供<string.h>/strcat 函數(shù)可以將 src 字符串中的內(nèi)容拼接到 dest 字符串的末尾:char *strcat(char *dest, const char *src);
),可以看到原先str2="world"被修改了。
sds的空間分配策略完全杜絕了發(fā)生緩沖區(qū)溢出的可能性: 當(dāng)sds API 需要對(duì)sds進(jìn)行修改時(shí), API 會(huì)先檢查sds的空間是否滿(mǎn)足修改所需的要求
, 如果不滿(mǎn)足的話(huà), API 會(huì)自動(dòng)將sds的空間擴(kuò)展至執(zhí)行修改所需的大小
, 然后才執(zhí)行實(shí)際的修改操作, 所以使用sds既不需要手動(dòng)修改sds的空間大小, 也不會(huì)出現(xiàn)前面所說(shuō)的緩沖區(qū)溢出問(wèn)題。
減少修改字符串時(shí)帶來(lái)的內(nèi)存重分配次數(shù)
每次增長(zhǎng)或者縮短一個(gè) C 字符串, 程序都總要對(duì)保存這個(gè) C 字符串的數(shù)組進(jìn)行一次內(nèi)存重分配操作。內(nèi)存重分配涉及復(fù)雜的算法, 并且可能需要執(zhí)行系統(tǒng)調(diào)用, 所以它通常是一個(gè)比較耗時(shí)的操作。
在sds中, buf 數(shù)組的長(zhǎng)度不一定就是字符數(shù)量加一, 數(shù)組里面可以包含未使用的字節(jié), 而這些字節(jié)的數(shù)量就由 SDS 的 alloc屬性 - len屬性來(lái)決定。
sds通過(guò)空間預(yù)分配和惰性空間釋放兩種優(yōu)化策略來(lái)減少修改字符串時(shí)帶來(lái)的內(nèi)存重分配次數(shù)。
空間預(yù)分配
空間預(yù)分配策略如下:
- 如果對(duì)sds進(jìn)行修改之后, len < 1 MB , 則alloc = 2 * len,buf長(zhǎng)度 = 2*len + 1;
- 如果對(duì)sds進(jìn)行修改之后, len >= 1 MB , 則alloc = len + 1MB,buf長(zhǎng)度 = len + 1MB + 1byte;
通過(guò)空間預(yù)分配策略, Redis 可以減少連續(xù)執(zhí)行字符串增長(zhǎng)操作所需的內(nèi)存重分配次數(shù)。
惰性空間釋放
惰性空間釋放用于優(yōu)化sds的字符串縮短操作: 當(dāng)需要縮短SDS保存的字符串時(shí),程序并不立即使用內(nèi)存重分配來(lái)回收縮短后多出來(lái)的字節(jié), 而是先預(yù)留著,并等待將來(lái)使用。
當(dāng)然, sds也提供了相應(yīng)的API,讓我們可以在有需要時(shí),真正地釋放 sds里面的未使用空間, 所以不用擔(dān)心惰性空間釋放策略會(huì)造成內(nèi)存浪費(fèi)。
二進(jìn)制安全
對(duì)于C語(yǔ)言來(lái)說(shuō),C 字符串中的字符必須符合某種編碼(比如 ASCII), 并且除了字符串的末尾之外, 字符串里面不能包含NULL字符, 否則最先被程序讀入的空字符將被誤認(rèn)為是字符串結(jié)尾 —— 這些限制使得 C 字符串只能保存文本數(shù)據(jù), 而不能保存像圖片、音頻、視頻、壓縮文件這樣的二進(jìn)制數(shù)據(jù)。
sds的 API 都是二進(jìn)制安全的(binary-safe): 所有sds API 都會(huì)以處理二進(jìn)制的方式來(lái)處理sds存放在buf數(shù)組里的數(shù)據(jù), 程序不會(huì)對(duì)其中的數(shù)據(jù)做任何額外的處理,數(shù)據(jù)在寫(xiě)入時(shí)是什么樣的,它被讀取時(shí)就是什么樣。也就是說(shuō)相比C語(yǔ)言字符串,SDS會(huì)通過(guò)len來(lái)限制讀取長(zhǎng)度,而非“\0”,所以保證了二進(jìn)制安全。
因此sds既可以保存文本數(shù)據(jù), 又能保存二進(jìn)制數(shù)據(jù)。
兼容部分 C 字符串函數(shù)
通過(guò)遵循C字符串以NULL字符結(jié)尾的慣例, sds可以使用部分 <string.h> 庫(kù)中的函數(shù)。