爲何要從Redis源碼分析
html
上篇咱們已經瞭解了Redis是什麼,在Linux上如何安裝,常見的數據類型和API使用,若是有不明白的,能夠移步到主頁。redis
Redis是使用C寫的,而C中根本不存在string,list,hash,set和zset這些數據類型,那麼C是如何將這些數據類型實現出來的呢?咱們從該篇開始,就要開始分析源碼啦😁。segmentfault
咱們這篇來學習string的底層實現,首先看下API的簡單應用,設置str1變量爲helloworld,而後咱們使用debug object +變量名的方式看下,注意標紅的編碼爲embstr。數組
若是咱們將str2設置爲helloworldhelloworldhelloworldhelloworldhell,字符長度爲44,再使用下debug object+變量名的方式看下,注意標紅的編碼爲embstr。安全
可是當咱們設置爲helloworldhelloworldhelloworldhelloworldhello,字符長度爲45,再使用debug object+變量名的方式看下,注意標紅的編碼爲raw。bash
最後咱們將str3設置爲整數100,再使用debug object+變量名的方式看下,注意標紅的編碼爲int。數據結構
因此Redis的string類型一共有三種存儲方式,當字符串長度小於等於44,底層採用embstr;當字符串長度大於44,底層採用raw;當設置是整數,底層則採用int。
app
全部類型的數據結構最外層都是RedisObject,這部分會說,先這樣大體瞭解下,由於這篇的重點不在這。若是字符串小於等於44,實際的數據和RedisObject在內存中地址相鄰,以下圖。curl
若是字符串大於44,實際的數據和RedisObject在內存中地址不相鄰,以下圖。函數
再次強調,這些不重要,之後會講,如今提下,只是爲了能讓Redis的String類型有個大體瞭解,先從總體把握。咱們今天要說的實際上是實際的數據,即上圖指針指向的位置😄。
其實的數據並非直接存儲,也有封裝,看下面的代碼就知道分爲五種,分別是sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64。sdshdr5和另外四種的區別比較明顯,sdshrd5其實對內存空間的更加節約。其餘四種乍一看都差很少,包括已用長度len,總長度alloc,標記flags(感受沒啥用,要是有知道的小夥伴,歡迎指教),實際數據buf。
//定義五種不一樣的結構體,sdshdr5,sdshdr8, sdshdr16,sdshdr32,sdshdr64
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; // 8位的標記
char buf[];//實際數據的指針
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 已使用長度 */
uint8_t alloc; /* 總長度*/
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len;
uint16_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len;
uint32_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len;
uint64_t alloc;
unsigned char flags;
char buf[];
};複製代碼
假設咱們設置某個字符串爲hello,那麼他SDS的可用長度len爲8,已用長度len爲6,以下圖。注意:Redis會根據具體的字符長度,選擇相應的sdshdr,可是各個類型都差很少,因此下圖加簡單畫了。
咱們能夠看到是對字符數組的再封裝,可是爲何呢,直接使用字符數組不是更簡單嗎?這要從C和Java語言的根本區別提及。
咱們都知道Java的字符串有提供length方法,列表有提供size方法,咱們能夠直接獲取大小。可是C卻不同,更偏向底層實現,因此沒有直接的方法使用。這樣就帶來一個問題,若是咱們想要獲取某個數組的長度,就只能從頭開始遍歷,當遇到第一個'\0'則表示該數組結束。這樣的速度太慢了,不能每次由於要獲取長度就變量數組。因此設計了SDS數據結構,在原來的字符數組外面增長總長度,和已用長度,這樣每次直接獲取已用長度便可。複雜度爲O(1)。
若是傳統字符串保存圖片,視頻等二進制文件,中間可能出現'\0',若是按照原來的邏輯,會形成數據丟失。因此能夠用已用長度來表示是否字符數組已結束。
在sds.h中寫了一些常見方法,好比計算sds的長度(即sdshdr的len),計算sds的空閒長度(即sdshdr的可用長度alloc-已用長度len),計算sds的可用長度(即sdshdr的alloc)等等。可是你們有沒有疑問,這不是一行代碼搞定的事嗎,爲啥要抽象出方法呢?那麼問題在於在上面,咱們有將sdshdr分爲五種類型,分別是sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64。那麼咱們在實際使用的時候,想要區分當前是哪一個類型,並取其相應字段或設置相應字段。
//計算sds對應的字符串長度,其實上取得是字符串所對應的哪一種sdshdr的len值
static inline size_t sdslen(const sds s) {
// 柔性數組不佔空間,因此倒數第二位的是flags
unsigned char flags = s[-1];
//flags與上面定義的宏變量7作位運算
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5://0
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8://1
return SDS_HDR(8,s)->len;//取上面結構體sdshdr8的len
case SDS_TYPE_16://2
return SDS_HDR(16,s)->len;
case SDS_TYPE_32://3
return SDS_HDR(32,s)->len;
case SDS_TYPE_64://5
return SDS_HDR(64,s)->len;
}
return 0;
}
//計算sds對應的空餘長度,其實上是alloc-len
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;
}
//設置sdshdr的len
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;
}
}
//給sdshdr的len添加多少大小
static inline void sdsinclen(sds s, size_t inc) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
{
unsigned char *fp = ((unsigned char*)s)-1;
unsigned char newlen = SDS_TYPE_5_LEN(flags)+inc;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
}
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len += inc;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len += inc;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len += inc;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len += inc;
break;
}
}
//獲取sdshdr的總長度
static inline size_t sdsalloc(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)->alloc;
case SDS_TYPE_16:
return SDS_HDR(16,s)->alloc;
case SDS_TYPE_32:
return SDS_HDR(32,s)->alloc;
case SDS_TYPE_64:
return SDS_HDR(64,s)->alloc;
}
return 0;
}
//設置sdshdr的總長度
static inline void sdssetalloc(sds s, size_t newlen) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
/* Nothing to do, this type has no total allocation info. */
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->alloc = newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->alloc = newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->alloc = newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->alloc = newlen;
break;
}
}
複製代碼
咱們經過sdsnew方法來建立對象,顯示經過判斷init是否爲空來肯定初始大小,接着調用方法sdsnew(這邊方法名同樣,可是參數不同,其爲方法的重載),先根據長度肯定類型(上面有提過五種類型,不記得的能夠往上翻),而後根據類型分配相應的內存資源,最後追加C語言的結尾符'\0'。
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen);//根據長度肯定類型
/*空字符串,用sdshdr8,這邊是經驗寫法,當想構造空串是爲了放入超過32長度的字符串 */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);//到下一個方法,已經把他們放在一塊兒了
unsigned char *fp; /* flags pointer. */
//分配內存
sh = s_malloc(hdrlen+initlen+1);
if (!init)
memset(sh, 0, hdrlen+initlen+1);
if (sh == NULL) return NULL;
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
//根據不一樣的類型,建立不一樣結構體,調用SDS_HDR_VAR函數
//爲不一樣的結構體賦值,如已用長度len,總長度alloc
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);
//最後追加'\0'
s[initlen] = '\0';
return s;
}
//根據實際字符長度肯定類型
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;
#endif
return SDS_TYPE_64;
}
複製代碼
String類型的刪除並非直接回收內存,而是修改字符,讓其爲空字符,這實際上是惰性釋放,等待未來使用。在調用sdsempty方法時,再次調用上面的sdsnewlen方法。
/*修改sds字符串使其爲空(零長度)。
*可是,全部現有緩衝區不會被丟棄,而是設置爲可用空間
*這樣,下一個append操做將不須要分配到
*當要縮短SDS保存的字符串時,程序並不當即使用內存充分配來回收縮短後多出來的字節,並等待未來使用。
void sdsclear(sds s) {
sdssetlen(s, 0);
s[0] = '\0';
}
sds sdsempty(void) {
return sdsnewlen("",0);
}複製代碼
添加字符串,sdscat輸入參數爲sds和字符串t,首先調用sdsMakeRoomFor擴容方法,再追加新的字符串,最後添加上結尾符'\0'。咱們來看下擴容方法裏面是如何實現的?第一步先調用常見方法中的sdsavail方法,獲取還剩多少空閒空間。若是空閒空間大於要添加的字符串t的長度,則直接返回,不想要擴容。若是空閒空間不夠,則想要擴容。第二步判斷想要擴容多大,這邊有分狀況,若是目前的字符串小於1M,則直接擴容雙倍,若是目前的字符串大於1M,則直接添加1M
。第三個判斷添加字符串以後的數據類型仍是否和原來的一致,若是一致,則沒啥事。若是不一致,則想要新建一個sdshdr,把現有的數據都挪過去。
這樣是否是有點抽象,舉個例子,如今str的字符串爲hello,目前是sdshdr8,總長度50,已用6,空閒44。如今想要添加長度爲50的字符t,第一步想要看下是否要擴容,50明顯大於44,須要擴容。第二步擴容多少,str的長度小於1M,因此擴容雙倍,新的長度爲50*2=100。第三步50+50所對應sdshdr類型仍是sdshdr8嗎?明顯仍是sdshdr8,因此不要數據遷移,還在原來的基礎上添加t便可。
sds sdscat(sds s, const char *t) {
return sdscatlen(s, t, strlen(t));
}
sds sdscatlen(sds s, const void *t, size_t len) {
//調用sds.h裏面的sdslen,即取已用長度
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;
}
sds sdsMakeRoomFor(sds s, size_t addlen) {
void *sh, *newsh;
//調用sds.h,獲取空閒長度alloc
size_t avail = sdsavail(s);
size_t len, newlen;
char type, oldtype = s[-1] & SDS_TYPE_MASK;
int hdrlen;
//空閒長度大於須要增長的,不須要擴容,直接返回
if (avail >= addlen) return s;
//調用sds.h裏面的sdslen,即取可用長度
len = sdslen(s);
sh = (char*)s-sdsHdrSize(oldtype);
//len加上要添加的大小
newlen = (len+addlen);
//#define SDS_MAX_PREALLOC (1024*1024)
//當新長度小於 1024*1024,直接擴容兩倍
if (newlen < SDS_MAX_PREALLOC)
newlen *= 2;
else //當新長度大於 1024*1024,加2014*1024
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 {//若是類型不同,從新開闢內存,把原來的數據複製過去 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; } //計算不一樣類型的結構體的大小 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; }複製代碼
該篇主要講了Redis的底層實現SDS,包括SDS是什麼,與傳統的C語言相比的優點,具體的邏輯圖,常見的方法(包括建立,刪除,擴容等)。同時也知道了Redis的embstr和raw的區別。若是以爲寫得還行,麻煩給個贊👍,您的承認纔是我寫做的動力!
若是以爲有說的不對的地方,歡迎評論指出。
好了,拜拜咯。