Redis源碼閱讀【1-簡單動態字符串】
Redis源碼閱讀【2-跳躍表】
Redis源碼閱讀【3-Redis編譯與GDB調試】
Redis源碼閱讀【4-壓縮列表】
Redis源碼閱讀【5-字典】
Redis源碼閱讀【6-整數集合】
Redis源碼閱讀【7-quicklist】
Redis源碼閱讀【8-命令處理生命週期-1】
Redis源碼閱讀【8-命令處理生命週期-2】
Redis源碼閱讀【8-命令處理生命週期-3】
Redis源碼閱讀【8-命令處理生命週期-4】
Redis源碼閱讀【番外篇-Redis的多線程】
建議搭配源碼閱讀:源碼地址
1、介紹
簡單動態字符串(Simple Dynamic Strings SDS)是Redis的基本數據結構之一,主要用於存儲字符串和整型數據。SDS兼容C語音標準字符串處理函數,並且在此保證了二進制安全。
二進制安全主要是針對類似於 \0 等有特殊含義的轉義字符保證其安全性,而且不損害其內容
2、SDS 基本結構
首先我們看看SDS在C語言中的基本結構體是怎麼樣的
struct sds {
int len; // buf 已經佔用的字節長度
int alloc; // 總長度 (不包括頭和空終止符)
char buf[]; // 數據空間
}
//這裏的成員屬性不一定就是使用 int 也可能是更大或者更小的數據類型
SDS基本結構如下圖所示(屬性長度不一定就是4):
之所以使用這種方式來存放字符串,是因爲SDS結構體中的地址是連續的,這樣能通過偏移量的方式快速查找內存內容,同時也能通過buf的地址非常快速獲取結構體SDS的首地址。
3、SDS 類型
從上面的圖片中看出,SDS 佔用的空間,除了本身buf 實際數據佔用的空間,還有 len alloc 等結構屬性也會佔用一定的空間大小,但是如果Redis中存儲了大量的短字符,那麼這種結構體的頭部無疑是對空間的浪費,而Redis本身就是主打性能和空間,這樣的空間浪費,是不能容忍的,於是Redis 對 SDS 做出了不同的劃分,分別如下:
//小於一字節
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags;
char buf[];
};
//一字節
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[];
};
//2字節
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len;
uint16_t alloc;
unsigned char flags;
char buf[];
};
//4字節
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len;
uint32_t alloc;
unsigned char flags;
char buf[];
};
//8字節
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len;
uint64_t alloc;
unsigned char flags;
char buf[];
};
//注意這樣都是使用uint 這樣能最大的使用空間
如上述代碼中展示的那樣,在結構體中引入了一個 flags 的單字節標記來區分SDS的類型
在單字節模式下 flags 的空間使用如下圖所示:
其中,低3位用來區分當前SDS的類型,高5位用來存儲當前SDS的數據長度,所以 SDS5 的範圍也只有【0~31】,flags 後面也就是實際的數據內容了,除此之外其它的 SDS 類型的頭結構基本上就是 len ,alloc ,flags 所以頭部空間基本上就是 S[len + alloc + flags],其中 len 和 alloc 根據不同類型的SDS使用不同的大小,以保證節約空間的目的,同時 flags 與 SDS5 一樣,只有前三位存儲類型,而後五位不存儲數據
__attribute__ ((__packed__))
需要關注這塊,結構體會按照其所有變量結構體做最小公倍數字節對齊。當使用 packed 修飾後,結構體會按照 1字節對齊。以 SDS32 爲例 ,修飾前按照 12(4x3)字節對齊,修飾後按照1字節對齊。
修飾前後內存空間如下圖所示。
這樣做有一下幾個好處:
1、節約內存:如SDS32可以節省3個字節
2、buf指針引用:SDS返回給上層的,不是結構體首地址,而是 buf 指針地址,這樣可以通過 buf[-1] 直接獲得 flags ,來識別當前 sds 結構體的類型,從而獲取整個結構體的任意一個部分
4、 創建字符串
Redis 通過 sdsnewlen 函數創建 SDS。函數會根據字符串長度來選擇合適的SDS 類型,待數據填入完成後,會返回 SDS buf 的指針作爲 SDS 的指針。
如下代碼:
/**
* 入參有兩個,一個是初始化字符串的指針,另一個是當前字符串的字節長度
*/
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
//根據長度選擇合適的sds類型
char type = sdsReqType(initlen);
//如果本身是空字符,那麼直接使用SDS8 而不是 SDS5 因爲 SDS5 不適合空字符
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
//計算當前類型SDS 頭部字節大小
int hdrlen = sdsHdrSize(type);
unsigned char *fp; /* flags 指針. */
// 按照 頭部空間 + 字符串大小 + 1 分配空間 (+1是爲了結束符號 \0)
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; // s 是指向 buf 的指針
fp = ((unsigned char*)s)-1; //s 是buf的指針 -1 即指向 flags
//按照類型初始化 SDS
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、釋放字符串
SDS提供了直接釋放內存的方法-sdsfree,該方法通過對 sds 指針的偏移,可以定位到 sds 的首部,然後調用 s_free釋放內存:
void sdsfree(sds s) {
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-1])); //這裏的s_free 就是 free
}
除此之外 sds 還提供了 sdsclear 方法去清空字符串,目的是爲了優化性能,不直接釋放內存,而是將sds的len設置爲0,新的數據可以在此之上覆蓋,從而不必再重新分配內存。
void sdsclear(sds s) {
sdssetlen(s, 0); //設置 len 爲0
s[0] = '\0'; // buf 直接設置爲結束字符
}
sdsclear 和 sdsfree 的差別是 sdsfree 會直接調用 free 是直接釋放內存的使用權,而 sdsclear只是清空,允許後續相近的字符串能在此之上進行使用,場景並不是很通用,但是性能上比 sdsfree 要好
6、拼接字符串
拼接字符是通過sdscatsds來實現的
sds sdscatsds(sds s, const sds t) {
return sdscatlen(s, t, sdslen(t));
}
sdscatsds 是封裝給上層使用的,sdscatlen纔是具體的實現。調用sdscatlen可能會發生擴容的場景,其中調用sdsMakeRoomFor去檢查字符串是否需要擴容,若無需擴容則直接返回,需要擴容會返回擴容後的 sds。
代碼如下:
/**
* 兩個入參,原sds 和 需要增加的空間大小
*/
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
size_t avail = sdsavail(s); // 查找當前 sds 剩餘可用空間的大小
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
//有足夠的空間不需要擴容
if (avail >= addlen) return s;
len = sdslen(s); //獲取當前sds 的長度
sh = (char *) s - sdsHdrSize(oldtype); //定位到 sds 的頭部 【buf地址 - 當前header長度】
newlen = (len + addlen); //獲得目標需要的總共大小空間
if (newlen < SDS_MAX_PREALLOC) //新長度大於 1mb 的按照 2倍擴容 SDS_MAX_PREALLOC 是最小分配大小
newlen *= 2;
else //新長度小於 1mb 的 按照 1mb 擴容 SDS_MAX_PREALLOC 是最小分配大小
newlen += SDS_MAX_PREALLOC;
type = sdsReqType(newlen); //按照新大小確定需要分配的sds類型
//強制把 sds5 變成 sds8 因爲 sds 5 是無法得知剩餘空間的 不支持擴容
if (type == SDS_TYPE_5) type = SDS_TYPE_8;
hdrlen = sdsHdrSize(type);//根據新的sds類型確定 頭部長度
//判斷新舊類型是否一樣
if (oldtype == type) {
newsh = s_realloc(sh, hdrlen + newlen + 1); //追加分配分配sds 的 buf 空間 realloc 擴大空間
if (newsh == NULL) return NULL;
s = (char *) newsh + hdrlen; //定位到新sds 的 buf 位置
} else {
newsh = s_malloc(hdrlen + newlen + 1); //分配新的sds空間
if (newsh == NULL) return NULL;
memcpy((char *) newsh + hdrlen, s, len + 1);
s_free(sh); //釋放舊的空間
s = (char *) newsh + hdrlen; //定位到 buf 位置
s[-1] = type;
sdssetlen(s, len); //初始化 len
}
sdssetalloc(s, newlen); //初始化 alloc
return s;
}
7、其餘的API
函數名 | 說明 |
---|---|
sdsempty | 創建一個空字符,長度爲0 內容爲"" |
sdsnew | 根據給定的C字符串創建sds |
sdsdup | 複製給定的sds |
sdsupdatelen | 手動刷新sds相關統計值 |
sdsRemoveFreeSpace | 縮容處理,與擴容相反 |
sdsAllocSize | 返回給定sds當前佔用內存大小 |
sdsgrowzero | 將sds擴容到指定長度,並用0填充新增加內容 |
sdscpylen | 將C字符複製到給定sds中 |
sdstrim | 從sds兩端清除所有給定字符 |
sdscmp | 比較兩個給定sds的實際大小 |
sdssplitlen | 按照給定的分隔符號對sds切分 |