Redis5.0原始碼解析(一)----------簡單動態字串(SDS)
基於Redis5.0
Redis 沒有直接使用 C 語言傳統的字串表示(以空字元結尾的字元陣列,以下簡稱 C 字串), 而是自己構建了一種名為簡單動態字串(simple dynamic string,SDS)的抽象型別, 並將 SDS 用作 Redis 的預設字串表示。
SDS定義
//<sds.h>
typedef char *sds;
/* 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[];
};
//flag低3位儲存sds型別,sdshdr5的flag高5位儲存buf長度
#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
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3
//過去指向sdshdr的指標,並賦給sh
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
//獲取指向sdshdr結構體的指標
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
//獲取sdshdr5的字串長度,即flag欄位中高5位儲存的值
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
SDS_HDR --> | len | alloc | flag | buf(sds)
|
unsigned char flags = sds[-1]
len
:sds的長度,不包括末尾結束符alloc
:分配的sds長度,不包括結束符flag
:sds型別buf
:sds實際存放位置
SDS 遵循 C 字串以空字元結尾的慣例, 儲存空字元的 1 位元組空間不計算在 SDS 的 len 屬性裡面, 並且為空字元分配額外的 1 位元組空間,遵循空字元結尾這一慣例的好處是, SDS 可以直接重用一部分 C 字串函式庫裡面的函式,末尾空字元的分配在sdsnewlen
函式裡(sh = s_malloc(hdrlen+initlen+1);
),下面有完整函式程式碼
SDS與C字串的區別
-
常數複雜度
O(1)
獲取字串長度
獲取一個 C 字串的長度, 程式必須遍歷整個字串, 對遇到的每個字元進行計數, 直到遇到代表字串結尾的空字元為止, 這個操作的複雜度為O(N)
SDS 在len
屬性中記錄了 SDS 本身的長度, 所以獲取一個 SDS 長度的複雜度僅為O(1)
-
杜絕緩衝區溢位
SDS 的空間分配策略完全杜絕了發生緩衝區溢位的可能性: 當 SDS API 需要對 SDS 進行修改時, API 會先檢查 SDS 的空間是否滿足修改所需的要求, 如果不滿足的話, API 會自動將 SDS 的空間擴充套件至執行修改所需的大小, 然後才執行實際的修改操作, 所以使用 SDS 既不需要手動修改 SDS 的空間大小, 也不會出現緩衝區溢位問題 -
減少修改字串時帶來的記憶體重分配次數
C 字串並不記錄自身的長度, 所以對於一個包含了 N 個字元的 C 字串來說, 這個 C 字串的底層實現總是一個 N+1 個字元長的陣列(額外的一個字元空間用於儲存空字元),每次增長或者縮短一個 C 字串, 程式都總要對儲存這個 C 字串的陣列進行一次記憶體重分配操作,通過未使用空間(alloc - len)
, SDS 實現了空間預分配和惰性空間釋放兩種優化策略-
空間預分配
如果對 SDS 進行修改之後, SDS 的長度(也即是len
屬性的值)將小於 1 MB , 那麼程式分配和len
屬性同樣大小的未使用空間, 這時 SDSalloc
屬性的值將是和len
屬性的2倍。 舉個例子, 如果進行修改之後, SDS 的len
將變成 13 位元組, 那麼程式也會分配 13 位元組的未使用空間, SDS 的buf
陣列的實際長度將變成 13 + 13 + 1 = 27 位元組(額外的一位元組用於儲存空字元)。如果對 SDS 進行修改之後, SDS 的長度將大於等於 1 MB , 那麼程式會分配 1 MB 的未使用空間。 舉個例子, 如果進行修改之後, SDS 的
len
將變成 30 MB , 那麼程式會分配 1 MB 的未使用空間, SDS 的 buf 陣列的實際長度將為 30 MB + 1 MB + 1 byte 。通過這種預分配策略, SDS 將連續增長 N 次字串所需的記憶體重分配次數從必定 N 次降低為最多 N 次。
-
惰性空間釋放
惰性空間釋放用於優化 SDS 的字串縮短操作: 當 SDS 的 API 需要縮短 SDS 儲存的字串時, 程式並不立即使用記憶體重分配來回收縮短後多出來的位元組, 而是使用 free 屬性將這些位元組的數量記錄起來, 並等待將來使用通過惰性空間釋放策略, SDS 避免了縮短字串時所需的記憶體重分配操作, 併為將來可能有的增長操作提供了優化
-
-
二進位制安全
C 字串中的字元必須符合某種編碼(比如 ASCII), 並且除了字串的末尾之外, 字串裡面不能包含空字元, 否則最先被程式讀入的空字元將被誤認為是字串結尾 —— 這些限制使得 C 字串只能儲存文字資料, 而不能儲存像圖片、音訊、視訊、壓縮檔案這樣的二進位制資料使用 SDS 來儲存之前提到的特殊資料格式就沒有任何問題, 因為 SDS 使用 len 屬性的值而不是空字元來判斷字串是否結束,通過使用二進位制安全的 SDS , 而不是 C 字串, 使得 Redis 不僅可以儲存文字資料, 還可以儲存任意格式的二進位制資料
-
相容部分 C 字串函式
SDS 的 API 都是二進位制安全的, 但它們一樣遵循 C 字串以空字元結尾的慣例: 這些 API 總會將 SDS 儲存的資料的末尾設定為空字元, 並且總會在為 buf 陣列分配空間時多分配一個位元組來容納這個空字元, 這是為了讓那些儲存文字資料的 SDS 可以重用一部分
C 字串 | SDS |
---|---|
獲取字串長度的複雜度為 O(N) | 獲取字串長度的複雜度為 O(1) |
API 是不安全的,可能會造成緩衝區溢位 | API 是安全的,不會造成緩衝區溢位 |
修改字串長度 N 次必然需要執行 N 次記憶體重分配 | 修改字串長度 N 次最多需要執行 N 次記憶體重分配 |
只能儲存文字資料 | 可以儲存文字或者二進位制資料 |
可以使用所有 <string.h> 庫中的函式 | 可以使用一部分 <string.h> 庫中的函式 |
SDS API
//<sds.h>
//獲取sds長度
static inline size_t sdslen(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
return SDS_HDR(8,s)->len;
case SDS_TYPE_16:
return SDS_HDR(16,s)->len;
case SDS_TYPE_32:
return SDS_HDR(32,s)->len;
case SDS_TYPE_64:
return SDS_HDR(64,s)->len;
}
return 0;
}
//獲取sds可用的位元組數
/* sdsalloc() = sdsavail() + sdslen() */
static inline size_t sdsavail(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5: {
return 0;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
return sh->alloc - sh->len;
}
}
return 0;
}
建立一個sds字串的核心函式:
/* Create a new sds string with the content specified by the 'init' pointer
* and 'initlen'.
* If NULL is used for 'init' the string is initialized with zero bytes.
* If SDS_NOINIT is used, the buffer is left uninitialized;
*
* The string is always null-termined (all the sds strings are, always) so
* even if you create an sds string with:
*
* mystring = sdsnewlen("abc",3);
*
* You can print the string with printf() as there is an implicit \0 at the
* end of the string. However the string is binary safe and can contain
* \0 characters in the middle, as the length is stored in the sds header. */
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen);
/* Empty strings are usually created in order to append. Use type 8
* since type 5 is not good at this. */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags pointer. */
//為結尾空字元多分配1位元組的空間
sh = s_malloc(hdrlen+initlen+1);
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
if (sh == NULL) return NULL;
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
switch(type) {
case SDS_TYPE_5: {
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
}
if (initlen && init)
memcpy(s, init, initlen);
s[initlen] = '\0';
return s;
}
擴容的核心函式:
//#define SDS_MAX_PREALLOC (1024*1024)
/* 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. */
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
/* Return ASAP if there is enough space left. */
if (avail >= addlen) return s;
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC) //SDS_MAX_PREALLOC (1024*1024) = 1M
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
type = sdsReqType(newlen);
/* Don't use type 5: the user is appending to the string and type 5 is
* not able to remember empty space, so sdsMakeRoomFor() must be called
* at every appending operation. */
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);
if (oldtype==type) {
newsh = s_realloc(sh, hdrlen+newlen+1);
if (newsh == NULL) return NULL;
s = (char*)newsh+hdrlen;
} else {
/* Since the header size changes, need to move the string forward,
* and can't use realloc */
newsh = s_malloc(hdrlen+newlen+1);
if (newsh == NULL) return NULL;
memcpy((char*)newsh+hdrlen, s, len+1);
s_free(sh);
s = (char*)newsh+hdrlen;
s[-1] = type;
sdssetlen(s, len);
}
sdssetalloc(s, newlen);
return s;
}
對給定字元或字串進行分割:
/* Split 's' with separator in 'sep'. An array
* of sds strings is returned. *count will be set
* by reference to the number of tokens returned.
*
* On out of memory, zero length string, zero length
* separator, NULL is returned.
*
* Note that 'sep' is able to split a string using
* a multi-character separator. For example
* sdssplit("foo_-_bar","_-_"); will return two
* elements "foo" and "bar".
*
* This version of the function is binary-safe but
* requires length arguments. sdssplit() is just the
* same function but for zero-terminated strings.
*/
sds *sdssplitlen(const char *s, ssize_t len, const char *sep, int seplen, int *count) {
int elements = 0, slots = 5;
long start = 0, j;
sds *tokens;
if (seplen < 1 || len < 0) return NULL;
//tokens相當於是一個二維陣列,預設先分配五個指標記憶體,即可以存5個被分割的字串
tokens = s_malloc(sizeof(sds)*slots);
if (tokens == NULL) return NULL;
if (len == 0) {
*count = 0;
return tokens;
}
for (j = 0; j < (len-(seplen-1)); j++) {
/* make sure there is room for the next element and the final one */
if (slots < elements+2) {
sds *newtokens;
slots *= 2;
newtokens = s_realloc(tokens,sizeof(sds)*slots);
if (newtokens == NULL) goto cleanup;
tokens = newtokens;
}
/* search the separator */
if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) {
tokens[elements] = sdsnewlen(s+start,j-start);
if (tokens[elements] == NULL) goto cleanup;
elements++;
start = j+seplen;
j = j+seplen-1; /* skip the separator */
}
}
/* Add the final element. We are sure there is room in the tokens array. */
tokens[elements] = sdsnewlen(s+start,len-start);
if (tokens[elements] == NULL) goto cleanup;
elements++;
*count = elements;
return tokens;
cleanup:
{
int i;
for (i = 0; i < elements; i++) sdsfree(tokens[i]);
s_free(tokens);
*count = 0;
return NULL;
}
}
返回指定範圍內的字串:
/* Turn the string into a smaller (or equal) string containing only the
* substring specified by the 'start' and 'end' indexes.
*
* start and end can be negative, where -1 means the last character of the
* string, -2 the penultimate character, and so forth.
*
* The interval is inclusive, so the start and end characters will be part
* of the resulting string.
*
* The string is modified in-place.
*
* Example:
*
* s = sdsnew("Hello World");
* sdsrange(s,1,-1); => "ello World"
*/
void sdsrange(sds s, ssize_t start, ssize_t end) {
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 >= (ssize_t)len) {
newlen = 0;
} else if (end >= (ssize_t)len) {
end = len-1;
newlen = (start > end) ? 0 : (end-start)+1;
}
} else {
start = 0;
}
if (start && newlen) memmove(s, s+start, newlen);
s[newlen] = 0;
sdssetlen(s,newlen);
}