Redis學習筆記——SDS

yoop發表於2017-03-30

Redis定義了一種資料結構動態字串來表示字串值,該資料結構的定義在檔案/src/sds.h中

/*
 * 儲存字串物件的結構
 */
struct sdshdr {
    
    // buf 中已佔用空間的長度
    int len;

    // buf 中剩餘可用空間的長度
    int free;

    // 資料空間
    char buf[];
}; 

變數名已經清晰的記錄了變數的作用。初次之外,還定義了這個結構的一些操作介面。

static inline size_t sdsavail(const sds s);        //返回字串生於的可用空間的長度,也就是free值
sds sdsnewlen(const void *init, size_t initlen);   //根據init指向的字串常量,和initlen指定的大小來構造字串
sds sdsnew(const char *init); //根據init指向的字串常量來構造字串,不過是通過sdsnewlen來實現的sds sdsempty(void); // 建立長度為0的空字串size_t sdslen(const sds s); //返回字串實際佔用空間的長度,也就是len值
sds sdsdup(const sds s); //拷貝一個字串,也是通過sdsnewlen來實現
sds sdsfree(sds s);   //釋放字串佔用的空間
sds sdsgrowzero(sds s, size_t len); //將sds擴充至指定長度,末尾未使用的空間以0填充
sds sdscatlen(sds s, const void *t, size_t len); //將字串t的前len個位元組填充到s的末尾
sds sdscat(sds s, const char *t); //將字串t填充到s的末尾,動下腦子就能猜到,內部通過sdscatlen實現
sds sdscatsds(sds s, const sds t); //同上,因為typedefchar *sds;
sds sdscpylen(sds s, const char *t, size_t len); //將t的前len個字元拷貝到s上,也就是會覆蓋s的內容
sds sdscpy(sds s, const char *t); // 將t的內容拷貝到s上
sds sdscatvprintf(sds s, const char *fmt, va_list ap);//通過fmt指定個格式來格式化字串
sds sdscatfmt(sds s, char const *fmt, ...); //將格式化後的任意數量個字串追加到s的末尾,通過sdscatvprintf實現
sds sdstrim(sds s, const char *cset); //對s的左右兩端進行裁剪,去掉cset指定的字元
void sdsrange(sds s, int start, int end); //通過索引區間[start,end]來擷取字串
void sdsupdatelen(sds s); //根據字串所佔用空間的長度大小來更新len、free
void sdsclear(sds s); //將字串的第一個字串置為` `,也就是把字串置為空字串,但是沒有釋放空間
int sdscmp(const sds s1, const sds s2); //比較兩個sds是否相等
sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count);
//使用分隔符sep對s程式進行分割,返回一個sds陣列,同時count設定為陣列的個數
//len和seplen分別是s和sep的長度
void sdsfreesplitres(sds *tokens, int count);//釋放陣列tokens的count個sds
void sdstolower(sds s); // 將sds的所有字元都轉換成小寫
void sdstoupper(sds s); // 將sds的所有字元都轉換成大寫 
sds sdsfromlonglong(long long value); //將長整型資料轉成字串
sds sdscatrepr(sds s, const char *p, size_t len);
//將長度為len的字串p以帶引號的格式追加到s的末尾
//如 s = "abc" , p = "gbdf
134"; 那麼函式的返回結果為 ret = "abc"gbdf\n134""
sds *sdssplitargs(const char *line, int *argc);//將一行文字分割成多個引數,引數的個數存在argc
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen);
// 將字串s中,出現存在from中指定的字元,都轉換成to中的字元,from與to是有位置關係,
// 假如from = "ckj", to = "345", 那麼‘c’就換成‘3’, `k`就換成‘4’, 以此類推
sds sdsjoin(char **argv, int argc, char *sep); //通過分隔符sep把字元陣列argv拼接成一個字串
sds sdsMakeRoomFor(sds s, size_t addlen); //對字串進行擴充,使之有addlen+1個長度的剩餘空間
void sdsIncrLen(sds s, int incr); //在不重新分配空間的基礎上,給字串增加incr長度
sds sdsRemoveFreeSpace(sds s); //回收sds剩餘的空間內容,但是不會修改字串的內容
size_t sdsAllocSize(sds s); //返回給s分配的記憶體的位元組數

程式碼中操作的物件是sds,並且多次利用sdshr中buf的偏移地址來獲取sdshr的地址,如下

struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));

個人感覺這個可以寫成一個巨集,像offsetof。之所以可以這樣用sizeof(struct sdshdr)是因為,buf是變長陣列,因為沒有指定長度,所以沒有佔用空間。所以,sizeof(struct sdshdr) == sizeof(int) + sizeof(int)
1.Redis通過空間預分配來減少修改字串帶來的記憶體重新分配的開銷。分配的演算法如下:
A. 如果對 SDS 進行修改之後, SDS 的長度(也即是 len 屬性的值)將小於 1 MB , 那麼程式分配和 len 屬性同樣大小的未使用空間, 這時 SDS len 屬性的值將和 free 屬性的值相同。 舉個例子, 如果進行修改之後, SDS 的 len 將變成 13 位元組, 那麼程式也會分配 13 位元組的未使用空間, SDS 的 buf 陣列的實際長度將變成 13 + 13 + 1 = 27 位元組(額外的一位元組用於儲存空字元)。
B. 如果對 SDS 進行修改之後, SDS 的長度將大於等於 1 MB , 那麼程式會分配 1 MB 的未使用空間。 舉個例子, 如果進行修改之後, SDS 的 len 將變成 30 MB , 那麼程式會分配 1 MB 的未使用空間, SDS 的 buf 陣列的實際長度將為 30 MB + 1 MB + 1 byte

2.Redis縮短字串時,只把字串的第一個字元置為`0`,不回收空間。也就是所謂的惰性空間釋放

3.Redis字串是二進位制安全的,因為sdshr裡的字串陣列 char buf[] 儲存字元的二進位制資料,通過len來表示大小

最後附上《Redis設計與實現的總結》裡指出的C字串跟SDS的區別
字串和 SDS 之間的區別
C 字串 SDS
獲取字串長度的複雜度為 O(N) 。 獲取字串長度的複雜度為 O(1) 。
API 是不安全的,可能會造成緩衝區溢位。 API 是安全的,不會造成緩衝區溢位。
修改字串長度 N 次必然需要執行 N 次記憶體重分配。 修改字串長度 N 次最多需要執行 N 次記憶體重分配。
只能儲存文字資料。 可以儲存文字或者二進位制資料。
可以使用所有 <string.h> 庫中的函式。 可以使用一部分 <string.h> 庫中的函式。

參考:《Redis設計與實現》

相關文章