本文的分析沒有特殊說明都是基於 Redis 6.0 版本原始碼
redis 6.0 原始碼:https://github.com/redis/redis/tree/6.0
在 Redis 中,字串都用自定義的結構簡單動態字串(Simple Dynamic Strings,SDS)。
Redis 中使用到的字串都是用 SDS,例如 key、string 型別的值、sorted set 的 member、hash 的 field 等等等等。。。
資料結構
舊版本的結構
在 3.2
版本之前,sds 的定義是這樣的:
struct sdshdr {
// buf 陣列中已使用的位元組數量,也就是 sds 本身的字串長度
unsigned int len;
// buf 陣列中未使用的位元組數量
unsigned int free;
// 位元組陣列,用於儲存字串
char buf[];
};
這樣的結構有幾個好處:
- 單獨記錄長度
len
,獲取字串長度的時間複雜度是 \(O(1)\) 。傳統的 C 字串獲取長度需要遍歷字串,直到遇到\0
,時間複雜度是 \(O(N)\)。 - buf 陣列末尾遵循 C 字串以
\0
結尾的慣例,可以相容 C 處理字串的函式。 - 減少修改字串帶來的記憶體重分配次數,Redis 使用了 空間預分配(預先申請大一點點的空間) 和 空間惰性釋放(字串變短修改
len
欄位即可)來減少字串修改引起的記憶體重新分配。 - 不以
\0
為結尾的判斷,二進位制安全。因為圖片等二進位制資料中,可能包含\0
,傳統 C 字串一遇到\0
就認為字串結束了,會導致不能完整儲存。
缺點:
len
和free
的定義用了 4 個位元組,可以表示2^32
的長度。但是我們實際使用的字串,往往沒有那麼長。4 個位元組造成了浪費。
新版本的結構
舊版本中我們說到,len
和 free
的缺點是用了太長的變數,新版本解決了這個問題。
我們來看一下新版本的 SDS
結構。
在 Redis 3.2 版本之後,Redis 將 SDS 劃分為 5 種型別:
型別 | 位元組 | 位 |
---|---|---|
sdshdr5 | < 1 | <8 |
sdshdr8 | 1 | 8 |
sdshdr16 | 2 | 16 |
sdshdr32 | 4 | 32 |
sdshdr64 | 8 | 64 |
新版本新增加了一個 flags
欄位來標識型別,長度 1 位元組(8 位)。
型別只佔用了前 3 位。在 sdshdr5
中,後 5 位用來儲存字串的長度。其他型別後 5 位沒有用。
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 前 3 位儲存型別,後 5 位儲存字串長度 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 字串長度,1 位元組 8 位 */
uint8_t alloc; /* 申請的總長度,1 位元組 8 位 */
unsigned char flags; /* 前 3 位儲存型別,後 5 位未使用 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* 字串長度,2 位元組 16 位 */
uint16_t alloc; /* 申請的總長度,2 位元組 16 位 */
unsigned char flags; /* 前 3 位儲存型別,後 5 位未使用 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* 字串長度,4 位元組 32 位 */
uint32_t alloc; /* 申請的總長度,4 位元組 32 位 */
unsigned char flags; /* 前 3 位儲存型別,後 5 位未使用 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* 字串長度,8 位元組 64 位 */
uint64_t alloc; /* 申請的總長度,8 位元組 64 位 */
unsigned char flags; /* 前 3 位儲存型別,後 5 位未使用 */
char buf[];
};
優點:
- 舊版本相對於傳統 C 字串的優點,新版本都有
- 相對於舊版本,新版本可以通過字串的長度,選擇不同的結構,可以節約記憶體
- 使用
__attribute__ ((__packed__))
,讓編譯器取消結構在編譯過程中的優化對齊,按照實際佔用位元組數進行對齊,可以節約記憶體
SDS 的初始化
sds 的定義,跟傳統的C語言字串保持型別相容 char *
。但是 sds 是二進位制安全的,中間可能包含\0
。
sds.h
typedef char *sds;
sds.c
// 初始化 sds
sds sdsnewlen(const void *init, size_t initlen) {
// 指向 sdshdr 開始地方的指標
void *sh;
// sds 實際是一個指標,指向 buf 開始的位置
sds s;
// 根據初始化的長度,返回 sds 的型別
char type = sdsReqType(initlen);
// initlen == 0,是空字串,空字串往往就是用來往後新增位元組的,使用 SDS_TYPE_8 比 SDS_TYPE_5 更好
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
// 根據型別獲取 struct sdshdr 的長度
int hdrlen = sdsHdrSize(type);
// flags 欄位的指標
unsigned char *fp;
// 開始分配空間,+1 是為了最後一個的結束符號 \0
sh = s_malloc(hdrlen+initlen+1);
if (sh == NULL) return NULL;
// const char *SDS_NOINIT = "SDS_NOINIT";
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
// 不是 init 則清空 sh 的記憶體
memset(sh, 0, hdrlen+initlen+1);
// s 指向了 buf 開始的地址
// 從上面結構可以看出,記憶體地址的順序: len, alloc, flag, buf
// 因為 buf 本身不佔用空間,hdrlen 實際上就是結構的頭(len、alloc、flags)
s = (char*)sh+hdrlen;
// flags 佔用 1 個位元組,所以 s 退一位就是 flags 的開始位置了
fp = ((unsigned char*)s)-1;
switch(type) {
case SDS_TYPE_5: {
// #define SDS_TYPE_BITS 3
// 前 3 位儲存型別,後 5 位儲存長度
*fp = type | (initlen << SDS_TYPE_BITS);
break;
}
case SDS_TYPE_8: {
// define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
// sh 變數賦值了 struct sdshdr
SDS_HDR_VAR(8,s);
sh->len = initlen;
sh->alloc = initlen;
*fp = type;
break;
}
// 下面是對 SDS_TYPE_16、SDS_TYPE_32、SDS_TYPE_64 的初始化,跟 SDS_TYPE_8 的類似,篇幅有限,省略...
}
// 如果 init 非空,則把 init 字串賦值給 s,實際上也是 buf 的初始化
if (initlen && init)
memcpy(s, init, initlen);
// 最後加一個結束標誌 \0
s[initlen] = '\0';
return s;
}
SDS 的擴/縮容
擴容
擴容就不跟初始化一樣寫註釋寫得那麼詳細了,直接拉最重要的幾句程式碼就行。
sds sdsMakeRoomFor(sds s, size_t addlen) {
// #define SDS_MAX_PREALLOC (1024*1024)
// 當新的長度小於 1M 的時候,長度會增長一倍
// 當新的長度達到 1M 之後,最多就增長 1M 了
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
// ...
}
縮容
sds 縮短不會真正縮小 buf,而是隻改長度而已,型別也不變。
sds.c
// 刪掉字串的左右字元中指定的字元
sds sdstrim(sds s, const char *cset) {
char *start, *end, *sp, *ep;
size_t len;
sp = start = s;
ep = end = s+sdslen(s)-1;
while(sp <= end && strchr(cset, *sp)) sp++;
while(ep > sp && strchr(cset, *ep)) ep--;
len = (sp > ep) ? 0 : ((ep-sp)+1);
if (s != sp) memmove(s, sp, len);
// 結尾符
s[len] = '\0';
// 縮短長度
sdssetlen(s,len);
return s;
}
sds.h
static inline void sdssetlen(sds s, size_t newlen) {
// 設定sds長度,只是修改 sdshdr 結構中的長度欄位,型別不會變
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
{
unsigned char *fp = ((unsigned char*)s)-1;
*fp = (unsigned char)(SDS_TYPE_5 | (newlen << SDS_TYPE_BITS));
}
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len = (uint8_t)newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len = (uint16_t)newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len = (uint32_t)newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len = (uint64_t)newlen;
break;
}
}