日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

Redis源码剖析(十)简单动态字符串sds

發(fā)布時(shí)間:2024/4/19 数据库 56 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Redis源码剖析(十)简单动态字符串sds 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

在對象系統(tǒng)概述中發(fā)現(xiàn),好像所有和字符串有關(guān)的內(nèi)容都有sds的存在,實(shí)際上,它是Redis內(nèi)部對于c字符串的封裝,所謂c字符串,其實(shí)就是char *,在sds.h頭文件中可以清楚的看到它的定義

//sds.h typedef char *sds;

所以創(chuàng)建sds類型變量實(shí)際上就是創(chuàng)建了一個(gè)char*變量,不過Redis字符串獨(dú)特的地方在于它記錄了字符串的長度。考慮想要計(jì)算c原生字符串的長度,需要使用strlen函數(shù),該函數(shù)會(huì)遍歷字符串直到遇到’\0’,復(fù)雜度是O(n),這容易造成性能瓶頸,所以Redis在創(chuàng)建字符串時(shí)記錄了字符串的長度,只需要O(1)即可計(jì)算得到

字符串結(jié)構(gòu)

Redis字符串不僅僅保存實(shí)際的數(shù)據(jù),還保存著用于記錄長度的頭部信息。假設(shè)要?jiǎng)?chuàng)建一個(gè)長度為n的字符串,那么Redis會(huì)申請大于n的內(nèi)存空間,其中,后半部分存儲(chǔ)字符串?dāng)?shù)據(jù),前半部分保存字符串頭部信息,Redis會(huì)根據(jù)字符串的長度選擇不同的頭部編碼

//sds.h /* * 字符串Header的定義,在字符串前面存放一個(gè)Header,用于記錄字符串的信息* __attribute__ ((__packed__))是通知編譯器使用緊湊模式分配內(nèi)存* 由于內(nèi)存對齊的原因,編譯器可能會(huì)在任意位置添加字節(jié)以滿足對齊條件* 該聲明告知編譯器在成員變量之間不能插入字節(jié),要插就在頭尾插* 原因是如果在中間插會(huì)破壞內(nèi)存結(jié)構(gòu),導(dǎo)致直接進(jìn)行char*加法無法準(zhǔn)確獲取數(shù)據(jù) * */ struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; char buf[]; };/* * 不同長度字符串采用不同的Header結(jié)構(gòu)以節(jié)約內(nèi)存* len : 字符串?dāng)?shù)據(jù)真正長度,已容納的字符數(shù),不包括結(jié)尾字符'\0'* alloc : 字符串?dāng)?shù)據(jù)的最大容量,可以容納多少個(gè)字符,不包括結(jié)尾字符'\0'* flags : Header的類型,總共5中,但是sdshdr5已經(jīng)不再使用* buf : 實(shí)際存儲(chǔ)字符串的位置*/ 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[]; };

其中,flags記錄著當(dāng)前頭部采用的編碼格式,由宏定義指出

//sds.h #define SDS_TYPE_5 0 #define SDS_TYPE_8 1 #define SDS_TYPE_16 2 #define SDS_TYPE_32 3 #define SDS_TYPE_64 4

這么做的原因是為了節(jié)省內(nèi)存,如果一個(gè)字符串的長度使用uint8_t就可以保存,那么使用uint64_t就顯得多余了,還記得嗎,Redis正在努力節(jié)省內(nèi)存

前面也看到了,sds就是char*的類型別名,所以平常使用的sds實(shí)際上是指sdshdr中的buf部分,那么怎么獲取頭部信息呢,Redis保證為sdshdr和buf中的字符串申請的內(nèi)存是連續(xù)的,也就是說如果sds指向buf的首地址,那么sds-1就指向頭部信息中的flags地址,sds-n就可以獲取長度和容量地址(n需要根據(jù)flags表示的編碼格式計(jì)算)

由于Redis中使用長度記錄字符串的結(jié)束位置,而不是依賴于’\0’,這使Redis中的字符串是二進(jìn)制安全的,因?yàn)樵诙M(jìn)制文件中,可能會(huì)存在多個(gè)’\0’,如果僅僅使用c語言原生字符串,那么很多數(shù)據(jù)都會(huì)丟失

字符串操作

創(chuàng)建字符串

字符串創(chuàng)建工作由sdsnewlen函數(shù)完成,函數(shù)首先根據(jù)字符串長度找到合適的頭部編碼,然后一次性申請頭部和字符串?dāng)?shù)據(jù)的內(nèi)存,完成初始化工作后,返回字符串?dāng)?shù)據(jù)

//sds.c /* 申請長度為initLen的字符串,如果init不為空,那么將init的數(shù)據(jù)復(fù)制到字符串中 */ sds sdsnewlen(const void *init, size_t initlen) {void *sh;sds s;/* 根據(jù)字符串長度選擇合適的頭部編碼 */char type = sdsReqType(initlen);/* sdshdr5已經(jīng)不再使用,默認(rèn)使用sdshdr8代替 */if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;/* 獲取選擇的頭部大小,用于申請內(nèi)存 */int hdrlen = sdsHdrSize(type);unsigned char *fp; /* flags pointer. *//* 宏定義,調(diào)用的是zmalloc, 加1是為了保存'\0' *//* 一次性申請可以保存頭部信息和字符串?dāng)?shù)據(jù)的內(nèi)存空間,確保內(nèi)存是連續(xù)的 */sh = s_malloc(hdrlen+initlen+1);/* 如果給定的地址是null,那么就初始化分配的內(nèi)存為0,否則,執(zhí)行拷貝工作 */if (!init)memset(sh, 0, hdrlen+initlen+1);if (sh == NULL) return NULL;/* sh指向頭部,為了獲取字符串?dāng)?shù)據(jù),需要跳過頭部內(nèi)存 * s指向?qū)嶋H用于保存數(shù)據(jù)的地址 */s = (char*)sh+hdrlen;/* s-1是Header中flags的地址 */fp = ((unsigned char*)s)-1;switch(type) {case SDS_TYPE_5: {*fp = type | (initlen << SDS_TYPE_BITS);break;}case SDS_TYPE_8: {/* 獲取不同類型的Header的指針 */SDS_HDR_VAR(8,s);/* 設(shè)置屬性,首次申請的容量和數(shù)據(jù)長度相等 */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;}}/* 如果給定地址不為null,執(zhí)行拷貝工作 */if (initlen && init)memcpy(s, init, initlen);/* 設(shè)置結(jié)束字符,多申請1個(gè)字節(jié)的原因 */s[initlen] = '\0';/* 返回字符串?dāng)?shù)據(jù)部分 */return s; }

SDS_HDR_VAR是宏定義,根據(jù)頭部編碼和字符串?dāng)?shù)據(jù)地址s獲取頭部地址,只需要數(shù)據(jù)地址減去頭部長度即可,其中##用于連接左右兩部分

//sds.h /* 獲取字符串Header的屬性值,##作用是將前后兩部分連在一起,即sdshdr##32 == sdshdr32(此時(shí)T為32) */ #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));

sdsReqType函數(shù)用于根據(jù)字符串長度選擇合適的頭部編碼

//sds.c /* 根據(jù)字符串長度選擇合適的頭部編碼,需要找到足夠容納并且最小的編碼格式 */ static inline char sdsReqType(size_t string_size) {/* 2的5次方,使用SDS_TYPE_5 */if (string_size < 1<<5)return SDS_TYPE_5;/* 2的8次方,使用uint8_t */if (string_size < 1<<8)return SDS_TYPE_8;/* 使用uint16_t */if (string_size < 1<<16)return SDS_TYPE_16;/* uint32_t */if (string_size < 1ll<<32)return SDS_TYPE_32;/* uint64_t */return SDS_TYPE_64; }

獲取字符串長度

獲取字符串長度只需要從頭部信息中讀取,時(shí)間復(fù)雜度為O(1)

//sds.h /* 獲取字符串長度,先獲取頭部編碼,然后地址前移,找到頭部地址,獲取長度 */ static inline size_t sdslen(const sds s) {/* 獲取頭部編碼 */unsigned char flags = s[-1];/* 選擇對應(yīng)的頭部編碼,獲取頭部地址,返回長度 */switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5:return SDS_TYPE_5_LEN(flags);case SDS_TYPE_8:return SDS_HDR(8,s)->len;case SDS_TYPE_16:return SDS_HDR(16,s)->len;case SDS_TYPE_32:return SDS_HDR(32,s)->len;case SDS_TYPE_64:return SDS_HDR(64,s)->len;}return 0; }

因?yàn)轭^部編碼類型的宏定義分別是0,1,2,3,4,所以這里flags與類型掩碼做與運(yùn)算只是讓flags的值在0到4之間,SDS_TYPE_MASK實(shí)際上是7

獲取字符串剩余容量

獲取字符串的剩余容量只需要用總?cè)萘繙p去已用容量,由sdsavail函數(shù)完成,函數(shù)大體和sdslen相同

//sds.h /* * 返回剩余可利用的內(nèi)存大小,利用Header中的alloc和len屬性* alloc : 保存字符串的總?cè)萘? len : 實(shí)際存儲(chǔ)的大小*/ 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);//和sdslen函數(shù)相比僅僅是返回?cái)?shù)據(jù)不同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; }

字符串容量擴(kuò)充

sdsMakeRoomFor函數(shù)用于將字符串?dāng)U充,擴(kuò)充的目的通常用于和其他字符串拼接

//sds.c /* 為s的字符串申請更大的容量已容納addLen個(gè)字節(jié)(Header中的alloc記錄當(dāng)前字符串的總?cè)萘? */ sds sdsMakeRoomFor(sds s, size_t addlen) {void *sh, *newsh;/* 獲取s字符串剩余可利用的字節(jié)大小,Header中的總?cè)萘?alloc) - 當(dāng)前大小(len) */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. *//* 如果可用空間足夠,直接返回,不需要擴(kuò)充 */if (avail >= addlen) return s;/* 返回s字符串的已用大小(Header中的len) */len = sdslen(s);/* 獲取字符串s的頭部地址 */sh = (char*)s-sdsHdrSize(oldtype);/* 新的容量等于當(dāng)前已用空間加擴(kuò)充大小 */newlen = (len+addlen);/* 每次擴(kuò)充都會(huì)申請多余空間以備不時(shí)之需 */if (newlen < SDS_MAX_PREALLOC)newlen *= 2;elsenewlen += SDS_MAX_PREALLOC;/* 為新容量找到一個(gè)合適的頭部編碼 */type = sdsReqType(newlen);if (type == SDS_TYPE_5) type = SDS_TYPE_8;/* 計(jì)算新編碼的Header大小 */hdrlen = sdsHdrSize(type);/* 如果新編碼和之前編碼相同,只需要擴(kuò)充數(shù)據(jù)即可* 否則,重新申請內(nèi)存(因?yàn)椴煌幋a的Header大小不同,同時(shí)也需要擴(kuò)充頭部) */if (oldtype==type) {/* 在原地址處重新申請內(nèi)存 */newsh = s_realloc(sh, hdrlen+newlen+1);if (newsh == NULL) return NULL;/* 獲取字符串?dāng)?shù)據(jù)地址 */s = (char*)newsh+hdrlen;} else {/* 重新申請新內(nèi)存 */newsh = s_malloc(hdrlen+newlen+1);if (newsh == NULL) return NULL;/* 將原數(shù)據(jù)拷貝到新內(nèi)存中 */memcpy((char*)newsh+hdrlen, s, len+1);/* 釋放原內(nèi)存 */s_free(sh);/* 獲取數(shù)據(jù)地址 */s = (char*)newsh+hdrlen;/* 設(shè)置Header編碼 */s[-1] = type;/* 設(shè)置字符串長度 */sdssetlen(s, len);}/* 更新字符串容量 */sdssetalloc(s, newlen);return s; }

連接兩個(gè)字符串

前面也說到了,擴(kuò)充容量通常是為了拼接其他字符串,由sdscatlen實(shí)現(xiàn)

//sds.c /* 連接兩個(gè)字符串 */ sds sdscatlen(sds s, const void *t, size_t len) {size_t curlen = sdslen(s);/* 為s擴(kuò)充容量,足以容納字符串t(長度為len) */s = sdsMakeRoomFor(s,len);if (s == NULL) return NULL;/* 將字符串復(fù)制到s的尾部 */memcpy(s+curlen, t, len);/* 更新字符串已用容量 */sdssetlen(s, curlen+len);/* 設(shè)置結(jié)束字符 */s[curlen+len] = '\0';return s; }

字符串容量縮減

擴(kuò)充和縮減是相反的兩個(gè)過程,代碼也非常相似,擴(kuò)充是令容量增加,縮減是令容量減小

//sds.c /* 將字符串末尾的空閑空間釋放掉,即另alloc == len */ sds sdsRemoveFreeSpace(sds s) {void *sh, *newsh;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;/* 獲取當(dāng)前字符串的長度len */size_t len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);/* 找到合適的Header編碼(有可能編碼變小) */type = sdsReqType(len);hdrlen = sdsHdrSize(type);/* 判斷Header的類型和原先是否相同,如果不相同,需要重新分配內(nèi)存* 因?yàn)镠edaer大小會(huì)改變 */if (oldtype==type) {newsh = s_realloc(sh, hdrlen+len+1);if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;} else {newsh = s_malloc(hdrlen+len+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, len);return s; }

對象系統(tǒng)中的sds

在之前對象系統(tǒng)的介紹中,發(fā)現(xiàn)很多地方用到了sds變量,現(xiàn)在就來回憶一下,順便填填坑

創(chuàng)建Raw類型和編碼的字符串對象

創(chuàng)建raw字符串由createRawStringObject函數(shù)完成,函數(shù)中調(diào)用sdsnewlen創(chuàng)建了一個(gè)sds字符串

//object.c /* 根據(jù)type和ptr創(chuàng)建編碼為raw字符串的對象 */ robj *createObject(int type, void *ptr) {/* 申請對象內(nèi)存空間 */robj *o = zmalloc(sizeof(*o));/* 設(shè)置類型,編碼,值,引用計(jì)數(shù)初始化為1 */o->type = type;o->encoding = OBJ_ENCODING_RAW;o->ptr = ptr;o->refcount = 1;/* 計(jì)算當(dāng)前時(shí)間,賦值給lru作為最后一次訪問時(shí)間 */o->lru = LRU_CLOCK();/* 返回對象指針 */return o; }/* 創(chuàng)建raw字符串類型變量 */ robj *createRawStringObject(const char *ptr, size_t len) {/* sdsnewlen()創(chuàng)建一個(gè)長度為len的sds字符串 */return createObject(OBJ_STRING,sdsnewlen(ptr,len)); }

可以看到,raw字符串調(diào)用了兩次動(dòng)態(tài)內(nèi)存申請,一次在sdsnewlen函數(shù)中申請頭部和字符串?dāng)?shù)據(jù)內(nèi)存,一次在createObject函數(shù)中申請robj內(nèi)存

創(chuàng)建Embstr類型和編碼的字符串對象

創(chuàng)建embstr字符串由createEmbeddedStringObject函數(shù)完成

//object.c /* 創(chuàng)建類型為embstr,編碼為embstr的字符串對象 */ robj *createEmbeddedStringObject(const char *ptr, size_t len) {/* 一次性創(chuàng)建robj和sds內(nèi)存* 因?yàn)閑mbstr僅僅用于長度較小的字符串,所以使用sdshdr8就足夠了*/robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);/* o是robj*類型,o+1實(shí)際加的字節(jié)數(shù)是sizeof(robj),得到的是sds頭部地址 */struct sdshdr8 *sh = (void*)(o+1);/* 設(shè)置類型,編碼,數(shù)據(jù),引用計(jì)數(shù),最后一次訪問時(shí)間 */o->type = OBJ_STRING;o->encoding = OBJ_ENCODING_EMBSTR;o->ptr = sh+1;o->refcount = 1;o->lru = LRU_CLOCK();/* 設(shè)置sds頭部信息,復(fù)制數(shù)據(jù)到sds中 */sh->len = len;sh->alloc = len;sh->flags = SDS_TYPE_8;if (ptr) {memcpy(sh->buf,ptr,len);sh->buf[len] = '\0';} else {memset(sh->buf,0,len+1);}return o; }

可以看到,embstr字符串只調(diào)用了一次動(dòng)態(tài)內(nèi)存申請,一次性申請了robj,sds頭部和字符串?dāng)?shù)據(jù)所需要的所有內(nèi)存,所以embstr通常用于長度較小的字符串編碼,因?yàn)閯?dòng)態(tài)內(nèi)存申請是很耗時(shí)的,尤其是申請大內(nèi)存時(shí)

小結(jié)

sds實(shí)際上就是char*類型,Redis在c原生字符串的基礎(chǔ)上添加了頭部信息,用于以O(shè)(1)的時(shí)間復(fù)雜度獲取字符串長度。字符串操作的實(shí)現(xiàn)都比較簡單,其中由于頭部信息和數(shù)據(jù)是連續(xù)存儲(chǔ)的,所以可以使用類似s[-n]的形式索引到頭部信息

總結(jié)

以上是生活随笔為你收集整理的Redis源码剖析(十)简单动态字符串sds的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。