1. 程式人生 > >Redis內部資料結構詳解之簡單動態字串(sds)

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原始碼方面的幫助。