字符串是Redis中一個重要的組成部分,Redis沒有直接使用C語言自帶的字符串,而是自身構(gòu)建了一個簡單動態(tài)字符串(Simple dynamic string, SDS)的抽象類型,該抽象類型不僅有額外的特性,還能兼容部分C語言內(nèi)建的字符串操作函數(shù)。
涉及的主要源代碼文件
sds.h
sds.c
SDS的定義
typedef char *sds; //聲明一個字符串指針類型的別名
//動態(tài)字符串結(jié)構(gòu)
//總長度 = len + free + 1 (1是末尾字符串\0所占的字節(jié))
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[]; //flexible array member,在計算struct長度是不算在內(nèi)
};
SDS字符串與C字符串相比,在結(jié)構(gòu)體中定義len
和free
屬性,len
用來記錄當前buf數(shù)組中已使用的字節(jié)空間,free
記錄了buf數(shù)組中未使用的字符串空間。SDS字符串與C字符串一樣,使用了\0
作為字符串結(jié)尾,但給\0
字符不算入len
中,因此buf數(shù)組的總大小應(yīng)為total = len + free + 1
。如圖中Redis
的SDS字符串buf分配的空間為10字節(jié)。\0
字節(jié)的添加完全有SDS底層函數(shù)負責,使用者無需關(guān)心,也由于這個\0
字符的存在,使得SDS可以重用一部分C字符串函數(shù)。
備注:sizeof(sdshdr) = 2 * sizeof(int64)
,buf所占的空間為0,可參考flexible array member
SDS的關(guān)鍵實現(xiàn)細節(jié)
//得到動態(tài)字符串鎖保存字符串的長度(不含\0)
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
//得到動態(tài)字符串的可用長度
static inline size_t sdsavail(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->free;
}
由于SDS中記錄了自身的長度len
,因此獲取字符串長度的時間復(fù)雜度為O(1),而不是C字符串的O(N),因此提高了獲取字符串長度的性能。從上面可以看到,len
和free
的獲取需要通過指針計算來獲取sdshdr
的實際地址來獲得的。
//指定長度初始化sds,init表示初始化填寫的內(nèi)容,init為空表示初始化字符串長度為0
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
//sdshdr的長度: strlen(sdshdr) + initlen + 1(1表示末尾0所占的資費)
if (init) {
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
} else {
sh = zcalloc(sizeof(struct sdshdr)+initlen+1); //初始化為0
}
if (sh == NULL) return NULL;
sh->len = initlen;
sh->free = 0;
if (initlen && init)
memcpy(sh->buf, init, initlen); //復(fù)制init指向地址的字符串
sh->buf[initlen] = '\0';
return (char*)sh->buf; //sds的指針為實際字符串的指針
}
從sdsnewlen
的返回可以看到,實際返回給上層的是buf數(shù)組的地址,對上層屏蔽了sdshdr
,sdshdr
的地址可以通過sdshder.buf
的地址來算出。
//增加sds的額外可存儲空間至addlen字節(jié),free = addlen
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; //當free空間足夠時,直接返回,無需重分配
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC) //預(yù)分配策略,若新字符長度小于1M,則為字符串分配2倍所需空間的大小
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC; //否則,只額外添加1M未使用空間
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
if (newsh == NULL) return NULL;
newsh->free = newlen - len;
return newsh->buf;
}
//字符串左右修剪函數(shù)
sds sdstrim(sds s, const char *cset) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
char *start, *end, *sp, *ep;
size_t len;
sp = start = s;
ep = end = s+sdslen(s)-1;
while(sp <= end && strchr(cset, *sp)) sp++; //修建左邊
while(ep > start && strchr(cset, *ep)) ep--; //修建右邊
len = (sp > ep) ? 0 : ((ep-sp)+1);
if (sh->buf != sp) memmove(sh->buf, sp, len); //移動內(nèi)存存儲內(nèi)容
//更新末尾、len、free
sh->buf[len] = '\0';
sh->free = sh->free+(sh->len-len);
sh->len = len;
return s;
}
//獲取子字符串,start/end 允許傳負數(shù)
void sdsrange(sds s, int start, int end) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
size_t newlen, len = sdslen(s);
if (len == 0) return;
if (start < 0) {
start = len+start;
if (start < 0) start = 0;
}
if (end < 0) {
end = len+end;
if (end < 0) end = 0;
}
newlen = (start > end) ? 0 : (end-start)+1;
if (newlen != 0) {
if (start >= (signed)len) {
newlen = 0;
} else if (end >= (signed)len) {
end = len-1;
newlen = (start > end) ? 0 : (end-start)+1;
}
} else {
start = 0;
}
if (start && newlen) memmove(sh->buf, sh->buf+start, newlen);
sh->buf[newlen] = 0;
sh->free = sh->free+(sh->len-newlen);
sh->len = newlen;
}
由于SDS字符串存在未使用空間,因此修改字符串長度不像C字符串,無需頻繁的通過內(nèi)存重分配來擴展或縮小字符串所占大小。這對于需要頻繁修改數(shù)據(jù)的Redis是一個極大的性能提升。sdsMakeRoomFor
函數(shù)用來對SDS字符串進行擴展,sdstrim
和sdsrange
用來對字符串進行縮減。
通過控制未使用空間,SDS實現(xiàn)了空間預(yù)分配和惰性釋放兩種空間優(yōu)化策略。
空間預(yù)分配
若對字符串進行擴展后的大小(newlen
)小于1M (1024*1024字節(jié))
,那么給SDS字符串額外分配所需大小一倍(擴展后大小為2*newlen
)的空間。若對字符串進行擴展后的大小大于1M
,則給SDS字符串額外分配1M空間。通過這種方式,減少了執(zhí)行字符串增長操作所需的內(nèi)存分配次數(shù)。惰性釋放
當SDS字符串進行縮減時,并不釋放多出來的空間,而是通過修改free屬性和len屬性來表示字符串的縮減,寵兒頻繁的內(nèi)存操作。
//連接指定長度字符串到sds末尾
sds sdscatlen(sds s, const void *t, size_t len) {
struct sdshdr *sh;
size_t curlen = sdslen(s);
s = sdsMakeRoomFor(s,len); //擴展空間
if (s == NULL) return NULL;
sh = (void*) (s-(sizeof(struct sdshdr)));
memcpy(s+curlen, t, len);
sh->len = curlen+len;
sh->free = sh->free-len;
s[curlen+len] = '\0'; //末尾添加0
return s;
}
SDS不使用C字符串函數(shù)的strcat
函數(shù),而是自己封裝了一個,當free
空間不足時,會擴展SDS字符串的未使用空間,然后在進行拼接,從而避免了緩沖區(qū)溢出。
總結(jié)
SDS字符串相比C字符串的優(yōu)勢:
- O(1)獲取字符串長度;
- 通過空間預(yù)分配和惰性空間釋放減少內(nèi)存的操作次數(shù);
- 杜絕緩沖區(qū)溢出
- 因為是通過len來控制字符串的長度,不依賴于
\0
,因此字符串是二進制安全的,不僅可以保存文本數(shù)據(jù),還可以用來保存任意格式的二進制數(shù)據(jù)。 - 因為SDS字符串末尾帶有
\0
,因此作為存儲普通字符串,可以使用部分C語言字符串函數(shù)。
SDS主要API
function | description | time complexity |
---|---|---|
sdslen | 獲取sds字符串長度 | O(1) |
sdsavail | 獲取sds可用長度 | O(1) |
sdsnewlen | 創(chuàng)建指定長度的sds,并接受C字符串為初始化內(nèi)容 | O(N) |
sdsnew | 根據(jù)給定的C字符串創(chuàng)建sds | O(N) |
sdsempty | 創(chuàng)建一個空的sds | O(1) |
sdsdup | 復(fù)制sds | O(N) |
sdsfree | 釋放sds | O(1) |
sdsgrowzero | 將sds擴展到指定長度,新擴展的內(nèi)容用\0 賦值 |
O(N) |
sdscatlen | 將一個給定字符串復(fù)制指定長度到sds末尾 | O(N) |
sdscat | 將一個給定字符串添加到sds末尾 | O(n) |
sdscatsds | 將一個sds添加到sds末尾 | O(N) |
sdscpylen | 將一個給定字符串復(fù)制一定長度到sds中 | O(N) |
sdscpy | 將一個給定字符串復(fù)制到sds中 | O(N) |
sdstrim | 修剪sds | O(M*N),M為sds長度,N為修剪內(nèi)容長度 |
sdsrange | 保留給定sds一定范圍的長度 | O(N) |
sdsupdatelen | 重新計算sds文本字符串的長度 | O(N) |
sdsclear | 清除sds為空字符串 | O(1) |
sdscmp | 比較sds字符串 | O(N) |
sdstolower | sds字符串轉(zhuǎn)為小寫 | O(N) |
sdstoupper | sds字符串轉(zhuǎn)為大寫 | O(N) |