Redis 在處理字串資料時,另外實現了一個名為 SDS (Simple Dynamic String)的資料結構。
前置知識
在 C 語言中,字串是由字元陣列構成,並在最後加入 \0
(NULL Byte)作為結束標記,這種實作方式存在一些缺點
- 非 Binary Safe:對於一些 UTF-8 字串可能中間會夾雜 NULL Byte,此時就會被當字串結尾
- 在 CVE-2006-7234 中,因為 PHP 實作上的缺陷導致可以在插入 NULL Byte 導致預期外行為
- 長度計算為 $O(n)$:在計算字串長度時,需要走訪字元陣列並找到
\0
- 實際上,標準函式庫的
strlen()
在計算字串長度時會利用資料對齊的特性所以執行效率會略高於每個字元比對 - 在比較現代的 CPU 上,還可以利用一些較為現代的指令集去加速比對
- 實際上,標準函式庫的
SDS
在 Redis 中,為了保證 Binary Safe 及效率問題,其核心實作了 SDS,定義於 sds.h
中
struct __attribute__((__packed__)) sdshdr5 {
unsigned char flags;
char buf[];
}
struct __attribute__((__packed__)) sdshdr8 {
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[];
}
- 另有
sdshdr16
,sdshdr32
,sdshdr64
分別使用uint16_t
,uint32_t
及uint64_t
作為len
及alloc
的資料型態
len 及 alloc
len
為有效字串長度,這使得 SDS 能夠在 $O(1)$ 複雜度下取得字串長度
alloc
表示記憶體分配時所申請的大小,即整個 SDS 的總大小。alloc - len
即為可使用的空閒空間。
flags
3 LSB(最小的 3 位元)代表型態,表示它是 sdshdr5
, sdshdr8
, sdshdr16
, sdshdr32
或 sdshdr64
在 sdshdr5
中,剩下的 5 MSB(最大的 5 位元)代表長度。因此, sdshdr5
僅能存放 32 bytes($2^5$)長度的字串;在其餘的型態中,剩下的 5 MSB 沒有作用。
沿革
在 2009 年 5 月初時 Redis 的 First Commit 中:
struct sdshdr {
long len;
long free;
char buf[0];
}
到 2009 年 5 月底時以 Flexible Array Member 改寫 buf
struct sdshdr {
long len;
long free;
char buf[];
}
直到 Redis 3.2 RC1(2015 年 12 月)才成為當前的型式。
SDS 的操作分析
typedef char *sds;
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. */
assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */
sh = s_malloc(hdrlen+initlen+1);
if (sh == NULL) return NULL;
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
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;
}