Redis內部資料結構詳解之簡單動態字串(sds)
本文所引用的原始碼全部來自Redis2.8.2版本。
Redis中簡單動態字串sds資料結構與API相關檔案是:sds.h, sds.c。
預備知識
下面介紹有關sizeof計算引數所佔位元組數的部分例項,方便下面對sds資料結構地址的計算理解
typedef struct Node{
int len;
char str[5];
}Node;
typedef struct Node2{
int len;
char str[];
}Node2;
sizeof(char*) = 4
sizeof(Node*) = 4
sizeof(Node) = 12
sizeof(Node2) = 4
簡單解釋下上述sizeof的結果值,前兩個等於4是因為指標;第三個值等於12是因為len佔4個位元組,char str[5]實際應該佔5個位元組,但是由於計算機記憶體對齊的原因其實際佔8個位元組;最後一個等於4,因為char str[]沒有實際長度,不被分配記憶體。
瞭解sizeof之後還需要了解stdarg.h中的va_list, va_start,va_end,va_copy的知識,這個在網上有很多就不多解釋了。
簡單動態字串sds與char*對比
sds在Redis中是實現字串物件的工具,並且完全取代char*.
char*的功能比較單一,不能實現Redis對字串高效處理的需求,char*的效能瓶頸主要在:計算字串長度需要使用strlen函式,該函式的時間複雜度是O(N),而在Redis中計算字串長度的操作十分頻繁,O(N)的時間複雜度完全不能接受,sds實現能在O(1)時間內得到字串的長度值;同時,在處理字串追加append操作時,如果使用char*則需要多次重新分配記憶體操作。
簡單動態字串sds資料結構
typedef char *sds;
struct sdshdr {
int len; //buf已佔用的長度,即當前字串長度值
int free; //buf空餘可用的長度,append時使用
char buf[]; //實際儲存字串資料
};
通過增加len欄位,就可以實現在O(1)時間複雜度內得到字串的長度,增加free欄位,在需要append字串時,如果free的值大於等於需要append的字串長度,那麼直接追加即可,不需要重新分配記憶體。sizeof(sdshdr) = 8.
簡單動態字串sds中函式API
函式名稱 |
作用 |
複雜度 |
sdsnewlen |
建立一個指定長度的sds,接受一個指定的C字串作為初始化值 |
O(N) |
sdsempty |
建立一個只包含空字串””的sds |
O(N) |
sdsnew |
根據給定的C字串,建立一個相應的sds |
O(N) |
sdsdup |
複製給定的sds |
O(N) |
sdsfree |
釋放給定的sds |
O(1) |
sdsupdatelen |
更新給定sds所對應的sdshdr的free與len值 |
O(1) |
sdsclear |
清除給定sds的buf,將buf初始化為””,同時修改對應sdshdr的free與len值 |
O(1) |
sdsMakeRoomFor |
對給定sds對應sdshdr的buf進行擴充套件 |
O(N) |
sdsRemoveFreeSpace |
在不改動sds的前提下,將buf的多餘空間釋放 |
O(N) |
sdsAllocSize |
計算給定的sds所佔的記憶體大小 |
O(1) |
sdsIncrLen |
對給定sds的buf的右端進行擴充套件或縮小 |
O(1) |
sdsgrowzero |
將給定的sds擴充套件到指定的長度,空餘的部分用\0進行填充 |
O(N) |
sdscatlen |
將一個C字串追加到給定的sds對應sdshdr的buf |
O(N) |
sdscpylen |
將一個C字串複製到sds中,需要依據sds的總長度來判斷是否需要擴充套件 |
O(N) |
sdscatprintf |
通過格式化輸出形式,來追加到給定的sds |
O(N) |
sdstrim |
對給定sds,刪除前端/後端在給定的C字串中的字元 |
O(N) |
sdsrange |
擷取給定sds,[start,end]字串 |
O(N) |
sdscmp |
比較兩個sds的大小 |
O(N) |
sdssplitlen |
對給定的字串s按照給定的sep分隔字串來進行切割 |
O(N) |
Redis中sds實現的細節解析
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
static inline size_t sdsavail(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->free;
}
上述兩個函式sdslen, sdsavail分別用來計算給定的sds的字串長度和給定的sds空餘的位元組數。仔細觀察會發現函式的引數是sds即char *,接著通過一行程式碼就能得到給定sds所對應的sdshdr資料結構,貌似很神奇的樣子啊!
看Redis中初始化一個sds的程式碼
/*init: C字串,initlen:C字串的長度*/
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
if (init) {
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
} else {
sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
}
if (sh == NULL) return NULL;
sh->len = initlen;
sh->free = 0;
if (initlen && init)
memcpy(sh->buf, init, initlen);
sh->buf[initlen] = '\0';
return (char*)sh->buf;
}
/* Create a new sds string starting from a null termined C string. */
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
核心函式是sdsnewlen,sh = zmalloc(sizeof(struct sdshdr)+initlen+1)為sdshdr資料結構分配記憶體,該段記憶體分為兩個部分:sdshdr資料結構所佔的記憶體數sizeof(sdshdr),我們知道其值為8;initlen+1為sdshdr資料結構中buf的記憶體。而sdsnewlen函式的返回值是buf的首地址,這樣在看sdslen函式,通過給定的sds首地址減去sizeof(sdshdr),那麼就應該是該sds所對應的sdshdr資料結構首地址,自然就能得到sh->len與sh->free。這種操作真的很神奇,這就是C語言指標的妙用,而且使用這種方式,很好的隱藏了sdshdr資料結構,對外介面全部同C字串類似,卻達到了求取sds字串長度時間複雜度O(1)與降低append操作頻繁申請記憶體的效果。
簡單動態字串sds空間擴充套件操作解析
sds模組的函式都比較簡單,不一一介紹,主要講解sds如何對空間進行擴充套件的,擴充套件操作主要在append操作的時候使用。
/* 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. */
//對sdshdr的buf進行擴充套件
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
size_t free = sdsavail(s); //檢視當前sds空餘的長度
size_t len, newlen;
if (free >= addlen) return s; //不需要擴充套件
len = sdslen(s); //得到當前sds字串的長度
sh = (void*) (s-(sizeof(struct sdshdr))); //得到sdshdr首地址
newlen = (len+addlen); //追加之後sds新的長度
if (newlen < SDS_MAX_PREALLOC) //SDS_MAX_PREALLOC (1024*1024),擴充套件的具體方法
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1); //重新分配記憶體
if (newsh == NULL) return NULL;//分配記憶體失敗
newsh->free = newlen - len; //新的sds空餘長度
return newsh->buf;
}
小結
Redis的簡單動態字串sds對比C語言的字串char*,有以下特性:
1) 可以在O(1)的時間複雜度得到字串的長度
2) 可以高效的執行append追加字串操作
3) 二進位制安全
sds通過判斷當前字串空餘的長度與需要追加的字串長度,如果空餘長度大於等於需要追加的字串長度,那麼直接追加即可,這樣就減少了重新分配記憶體操作;否則,先用sdsMakeRoomFor函式先對sds進行擴充套件,按照一定的機制來決定擴充套件的記憶體大小,然後再執行追加操作,擴充套件後多餘的空間不釋放,方便下次再次追加字串,這樣做的代價就是浪費了一些記憶體,但是在Redis字串追加操作很頻繁的情況下,這種機制能很高效的完成追加字串的操作。
由於sds其他的函式比較簡單,如果有問題的可以在回覆中提出。
指出一點2.8原始碼中sds作者作出的註釋有一處是錯誤的,具體就不列出了。
最後感謝黃健巨集(huangz1990)的Redis設計與實現及其他對Redis2.6原始碼的相關注釋對我在研究Redis2.8原始碼方面的幫助。