redis 版本:5.0
本文代碼在Redis源碼中的位置:redis/src/sds.c、redis/src/sds.h
源碼整體結構
src:核心實現代碼,用 C 語言編寫
tests:單元測試代碼,用 Tcl 實現
deps:所有依賴庫
字符串存儲結構
Redis 將字符串的實現稱爲 sds(simple dynamic string)。爲了提高存儲空間的利用率,Redis 對不同長度的字符串,採用不同的數據結構。
以下是長度小於32的字符串的存儲結構:
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags;
char buf[];
};
其中,flags
:高5位指示字符串真實長度,低3位指示數據類型;buf
:柔性數組,指向字符串存儲地址。
注:柔性數組是在 C99 及以上標準才支持,其目的是爲了在結構體中能夠“動態”地設定數組的長度。但需要注意的是,對結構體進行
sizeof
時,柔性數組的大小不被計算在內。因此在對結構體分配大小時,需要注意加上柔性數組的大小。柔性數組必須被聲明在結構體的最後,其起始地址與上一字段的末尾地址相連。
其他長度的字符串的存儲結構與其類似,結構分別如下:
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[];
};
其中,len
表示字符串實際長度;alloc
表示申請的字符數組的長度。
可以看出,Redis 最長能存儲長度大約爲 \(1.84* 10^{19}\) (\(2^{64}-1\)) 的字符串。
注:在聲明結構體時,
struct __attribute__ ((__packed__))
語法用來告訴編譯器採用緊湊的方式存儲數據,即1字節對齊,而不是默認的所有變量大小的最小公倍數做字節對齊。以
sdshdr32
爲例,若採用1字節對齊,len
,alloc
,flags
一共佔9個字節(4+4+1),而默認會使用4字節對齊,這樣flags
也會佔用4個字節,一共佔用12個字節。採用 packed 的好處是明顯的,一來可以減少數據的大小,提高空間利用率;二來這爲地址計算帶來了方便:無論哪種類型(除
sdshdr5
外),使用buff[-1]
即能獲取到flag
。
字符串基本操作
創建
sdsnewlen
方法負責創建字符串,代碼如下:
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen);
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);
unsigned char *fp;
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;
}
以上代碼比較簡單,但有幾點需要說明:
- 從第5行代碼可以看出,對於空字符串,Redis 將其做爲
sdshdr8
類型而不是sdshdr5
類型。其給出的原因是:用戶很有可能會在空字符串後追加新的字符串,因此用一個存儲長度適中的結構。 sh
指針指向對應結構體的起始地址,其長度爲結構體大小 + 待存儲字符串的大小 + 1。最後加1是爲了在末尾追加一個\0
符。s
指針即爲以上提到的柔性數組起始地址。fp
指針地址正好在s
指針指向地址的前一字節,指向flags
。- 第19行代碼中,
initlen << SDS_TYPE_BITS
將initlen
左移3位,正好印證之前提到的flags
使用高5位存儲字符串長度。之後或上type
,即把低3位置爲type
的值(因爲之前左移3位後,低3位值均爲0,這樣或運算的值就是type
)。 SDS_HDR_VAR
宏定義爲#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
。其中的##
表示字符連接的意思。以23行代碼爲例,該行代碼在經編譯器預處理後,變爲struct sdshdr8 *sh = (void*)((s)-(sizeof(struct sdshdr8)))
,語義爲sh = s - sizeof(sdshdr8)
。暫時還沒弄明白這裏爲啥要重算一下sh
。按理說第15行代碼s = (char*)sh+hdrlen;
中,s
正是由sh +sizeof(sdshdr8)
得到。- 注意,最終返回給外部的是
s
指針而不是結構體指針。
其中的sdsReqType
方法用於根據待存儲字符串長度選擇合適的存儲類型,代碼如下:
static inline char sdsReqType(size_t string_size) {
if (string_size < 1<<5)
return SDS_TYPE_5;
if (string_size < 1<<8)
return SDS_TYPE_8;
if (string_size < 1<<16)
return SDS_TYPE_16;
#if (LONG_MAX == LLONG_MAX)
if (string_size < 1ll<<32)
return SDS_TYPE_32;
return SDS_TYPE_64;
#else
return SDS_TYPE_32;
#endif
}
注:
LONG_MAX
表示long
類型整數最大值,與編譯器有關。通常64位編譯器值爲 \(2^{63} - 1\),32位編譯器爲 \(2^{32}-1\)。LLONG_MAX
表示long long
類型整數最大值,通常32位和64位編譯器值均爲 \(2^{63} - 1\)。參考:
注:static inline 關鍵字是在建議編譯器,以類似於宏定義的方式對待該函數,即在編譯階段,直接將該函數的相關指令插入到調用該函數的地方。這樣做可減少函數調用的開銷。
sdsHdrSize
函數用於計算對應存儲類型的大小,代碼如下:
static inline int sdsHdrSize(char type) {
switch(type&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return sizeof(struct sdshdr5);
case SDS_TYPE_8:
return sizeof(struct sdshdr8);
case SDS_TYPE_16:
return sizeof(struct sdshdr16);
case SDS_TYPE_32:
return sizeof(struct sdshdr32);
case SDS_TYPE_64:
return sizeof(struct sdshdr64);
}
return 0;
}
其中,SDS_TYPE_MASK
的值爲7,二進制即爲:0000 0111
,type
和它做與運算,正好得出自己的第3位的值,即類型值。
刪除
刪除字符串的方法有兩種,一種是真正釋放了內存,代碼如下:
void sdsfree(sds s) {
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-1]));
}
其中 s[-1]
之前也提到過,就是 flags
的值。而 s-sdsHdrSize(s[-1])
則得到了 sdshdr
結構體的起始地址。
另一種則只是將長度置爲0,並沒有真正釋放內存,這麼做的目的當然是爲了下次存儲字符串時,無需重新申請內存,直接再用即可。
void sdsclear(sds s) {
sdssetlen(s, 0);
s[0] = '\0';
}
其中,sdssetlen
函數用來設置字符串長度,代碼如下:
static inline void sdssetlen(sds s, size_t newlen) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
{
unsigned char *fp = ((unsigned char*)s)-1;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
}
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len = newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len = newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len = newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len = newlen;
break;
}
}
追加(cat)
追加操作用於在一個字符串後面添加另一字符串,其代碼如下:
sds sdscatsds(sds s, const sds t) {
return sdscatlen(s, t, sdslen(t));
}
連接後,s
將指向連接後的字符串。
sdscatlen
函數邏輯也很簡單:擴容(若需要) -> 複製追加內容 -> 修改長度。代碼如下:
sds sdscatlen(sds s, const void *t, size_t len) {
size_t curlen = sdslen(s);
s = sdsMakeRoomFor(s,len);
if (s == NULL) return NULL;
memcpy(s+curlen, t, len);
sdssetlen(s, curlen+len);
s[curlen+len] = '\0';
return s;
}
其中 sdsMakeRoomFor
爲擴容函數,根據剩餘可用空間大小和待追加的字符串長度決定是否擴容。它保證了其返回的指針指向的內存區域,一定能容納追加的字符串(若內部的內存申請成功的話)。代碼如下:
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)
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;
}
其基本邏輯爲:
- 若剩餘容量足夠容納待追加的字符串,則無需擴容(第9行)。
- 否則,算出追加後的字符串的長度
newlen
(第13行)。第13行~第17行代碼還對newlen
做了調整。暫時不知道爲啥要這麼做。 - 之後根據
newlen
決定使用什麼類型進行存儲。若類型還和之前一樣,則繼續沿用之前的內存結構,但需要使用s_realloc
擴大之前的內存空間(第28行);否則則需要重新申請一塊新的內存(第34行),將之前的數據複製到新內存中(第36行)。
連接(join)
連接操作是將一個字符串數組的所有元素串聯在一起,若給定了分隔符,各元素之間還需插入分隔符。例如,若字符串數組 s = ["a", "b", "c"]
,分隔符 sep = ","
,則 join(s, sep)
的結果爲:a,b,c
。其代碼如下:
sds sdsjoin(char **argv, int argc, char *sep) {
sds join = sdsempty();
int j;
for (j = 0; j < argc; j++) {
join = sdscat(join, argv[j]);
if (j != argc-1) join = sdscat(join,sep);
}
return join;
}
代碼很簡單,就不多做說明了。其中,sdsempty()
是構造一個空串,即:sdsnewlen("",0)
分割參數
sdssplitargs
函數用於將一條命令按參數分割成一個字符串數組,代碼如下:
sds *sdssplitargs(const char *line, int *argc) {
const char *p = line;
char *current = NULL;
char **vector = NULL;
*argc = 0;
while(1) {
/* skip blanks */
while(*p && isspace(*p)) p++;
if (*p) {
/* get a token */
int inq=0; /* set to 1 if we are in "quotes" */
int insq=0; /* set to 1 if we are in 'single quotes' */
int done=0;
if (current == NULL) current = sdsempty();
while(!done) {
if (inq) {
if (*p == '\\' && *(p+1) == 'x' &&
is_hex_digit(*(p+2)) &&
is_hex_digit(*(p+3)))
{
unsigned char byte;
byte = (hex_digit_to_int(*(p+2))*16)+
hex_digit_to_int(*(p+3));
current = sdscatlen(current,(char*)&byte,1);
p += 3;
} else if (*p == '\\' && *(p+1)) {
char c;
p++;
switch(*p) {
case 'n': c = '\n'; break;
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case 'b': c = '\b'; break;
case 'a': c = '\a'; break;
default: c = *p; break;
}
current = sdscatlen(current,&c,1);
} else if (*p == '"') {
/* closing quote must be followed by a space or
* nothing at all. */
if (*(p+1) && !isspace(*(p+1))) goto err;
done=1;
} else if (!*p) {
/* unterminated quotes */
goto err;
} else {
current = sdscatlen(current,p,1);
}
} else if (insq) {
if (*p == '\\' && *(p+1) == '\'') {
p++;
current = sdscatlen(current,"'",1);
} else if (*p == '\'') {
/* closing quote must be followed by a space or
* nothing at all. */
if (*(p+1) && !isspace(*(p+1))) goto err;
done=1;
} else if (!*p) {
/* unterminated quotes */
goto err;
} else {
current = sdscatlen(current,p,1);
}
} else {
switch(*p) {
case ' ':
case '\n':
case '\r':
case '\t':
case '\0':
done=1;
break;
case '"':
inq=1;
break;
case '\'':
insq=1;
break;
default:
current = sdscatlen(current,p,1);
break;
}
}
if (*p) p++;
}
/* add the token to the vector */
vector = s_realloc(vector,((*argc)+1)*sizeof(char*));
vector[*argc] = current;
(*argc)++;
current = NULL;
} else {
/* Even on empty input string return something not NULL. */
if (vector == NULL) vector = s_malloc(sizeof(void*));
return vector;
}
}
err:
while((*argc)--)
sdsfree(vector[*argc]);
s_free(vector);
if (current) sdsfree(current);
*argc = 0;
return NULL;
}
以上代碼總體上是在對字符串 line
逐個字符進行遍歷,指針 p
做爲遊標指向當前訪問的字符。
主體上,代碼由兩層 while
循環構成。外層 while
循環(第7行)條件總是成立,因此該函數要麼出錯結束(第102行~末尾),返回 NULL
,要麼從第98行成功結束,此時的條件是 p
指針正好指向字符串末尾的 \0
,返回分割出的參數數組 vector
。而內層 while
循環執行結束後(第91行),會識別出一個參數,並會將該參數添加到 vector
數組中(第92行)。
再來看內層 while
循環是如何識別一個參數的。這段代碼(第17行到89行)總體結構是 if...else if ... else ...
。進入前兩分支的條件是處於雙引號模式或單引號模式下,否則進入 else
分支。每輪循環結束,遊標 p
向後挪動一位(第88行)。
雙、單引號模式由 inq
和 insq
變量指示,前者表示當前訪問的字符在單引號內(即之前已訪問了起始單引號,但還未訪問到閉合單引號),後者則表示是在雙引號內。進入這兩個模式的時機當然是遇到了雙引號(第77行)或單引號(第80行)。
在非雙、單引號模式下,若遇到一般字符,都會將該字符追加到 current
字符串中(current
在遍歷開始是空串);若遇到 空格
、'\n'
、'\r'
、'\t'
、'\0'
,內層循環都將結束,並將 current
做爲識別到的一個參數。
在雙、單引號模式下,若是遇到 "\n"
、"\r"
、"\t"
、"\b"
,"\a"
(注意,這些都是兩個字符而不是一個),會將其做爲單字符 '\n'
、'\r'
、'\t'
、'\b'
、'\a'
加入 current
中(第41行);若字符串遍歷完成都沒遇到閉合的雙、單引號,則會報錯(第49行和第64行);閉合的雙、單引號後面不是空格(若字符串後面還有其他字符),也會報錯(第44行和第60行)。
另外,在雙引號模式下,會將格式類似於 "\x41"
的子串,做爲16進制數,轉成字符類型後追加到 current
中(第19行~第28行)。該例中,"\x41"
將做爲字母 A
。
以上便是 Redis 字符串操作最主要的函數,還有一些比如字符串大小寫轉換,比較等函數實現均非常簡單,這裏不再贅述,有興趣可以去看看源碼。