對於上一篇文章,我又本身總結概括並補充了一下,有了第二篇。redis
開始以前,咱們先準備點東西:位運算數組
i<<n 總結爲 i*2^n
因此緩存
1<<5 = 2^5數據結構
1<<8 = 2^8curl
1<<16 = 2^16this
1<<32 = 2^32url
1<<64 = 2^643d
Redis 3.2 之後SDS數據類型有5個指針
#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
結合上面的位運算,咱們也能理解這5個數據類型的命名規則。code
咱們如今有定義了5種SDS數據類型,那麼如何根據字符串長度找這些類型呢?
或者說輸入的字符串長度和類型有什麼關係?下面咱們來看一看他們之間的關係。
再來看看源碼:
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; return SDS_TYPE_64; #else return SDS_TYPE_32; #endif }
根據位運算左移公式,我能夠得知 1<<8 = 2^8 = 256
那麼這裏的 256是指什麼?這裏的256就是字節
也就是說:
SDS_TYPE_5 -- 32 Byte
SDS_TYPE_8 -- 256 Byte
SDS_TYPE_16 -- 64KB
SDS_TYPE_32 -- ...
SDS_TYPE_64 -- ...
如今數據類型找到了,咱們再來看看比較典型的幾種操做。
從使用角度講,追加通常用的頻率不多。因此有多大分配多大。
因此這裏追加的話,有兩種大狀況:還有剩餘 或 不夠用
主要講一下不夠用就要從新申請內存,那麼咱們如何去申請內存呢?
這裏提供了兩種分配策略:
<1M ,新空間 = 2倍擴容; >1M , 新空間 = 累加1M
空間有了,那麼咱們須要根據最新的空間長度佔用,再找到對應的新的SDS數據類型。
看一下源碼,增長一下印象:
/* 追加字符串*/ sds sdscatlen(sds s, const void *t, size_t len) { // 當前字符串長度 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; // 當前剩下的空間 size_t avail = sdsavail(s); size_t len, newlen; char type, oldtype = s[-1] & SDS_TYPE_MASK; int hdrlen; /* 空間足夠 */ if (avail >= addlen) return s; // 長度 len = sdslen(s); // 真正的數據體 sh = (char*)s-sdsHdrSize(oldtype); // 新長度 newlen = (len+addlen); // < 1M 2倍擴容 if (newlen < SDS_MAX_PREALLOC) newlen *= 2; // > 1M 擴容1M else newlen += SDS_MAX_PREALLOC; // 獲取sds 結構類型 type = sdsReqType(newlen); // type5 默認轉成 type8 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; }
外部字符串類型,找到了SDS結構,如今到了SDS轉內部結構
對於字符串類型爲何會分 embstr 和 raw呢?
咱們先說一下內存分配器:jemalloc、tcmalloc
這來能爲仁兄呢分配內存的大小都是 2/4/8/16/32/64 字節
對於redis 來說如何利用並適配好內存分配器依然須要好好計算一下。
Redis 給咱們實現了不少內部數據結構,這些內部數據結構得有本身的字描述文件-內部結構頭對象
不一樣對象有不一樣的type,同一個對象有不一樣的存儲形式,還有lru緩存淘汰機制信息,引用計數器,指向數據體的指針。
typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; int refcount; void *ptr; } robj;
因此SDS和 內部類型的關係相似於這樣的:
連續內存,和非連續內存
SDS爲何會是這樣的兩種內部結構呢?
回憶一下上面提到的:SDS結構,最小的應該是 SDS_TYPE_8(SDS_TYPE_5默認轉成8)
struc SDS{ int8 capacity; // 1字節 int8 len; // 1字節 int8 flags; // 1字節 byte[] content; // 內容 }
因此從上代碼看出,一個最小的SDS,至少佔用3字節.
還有內部結構頭:RedisObject
typedef struct redisObject { unsigned type:4; // 4bit unsigned encoding:4; // 4bit unsigned lru:LRU_BITS; // 24bit int refcount; // 4字節 void *ptr; // 8字節 } robj;
16字節 = 32bit(4字節) + 4字節 + 8字節
因此一個內部類型頭指針大小爲:16字節
再加上最小SDS的3字節,一共 19字節。也就是說一個最小的字符串所佔用的內存空間是19字節
還記得上面咱們提到過的內存分配器麼?(2/4/8/16/32/64 字節)
對,若是要給這個最小19字節分配內存,至少要分配一個32字節的內存。固然若是字符串長一點,再往下就能夠分配到64字節的內存。
以上這種形式被叫作:embstr,這種形式使得 RedisObject和SDS 內存地址是連續的。
那麼一旦大於64字節,形式就變成了raw,這種形式使得內存不連續,由於SDS已經變大,取得大的連續內存得不償失。
再回來討論一下 embstr, 最大64字節內存分配下來,咱們實際能夠真正存儲字符串的長度是多少呢?--44字節
64字節,減去RedisObject頭信息19字節,再減去3字節SDS頭信息,剩下45字節,再去除\0結尾。這樣最後能夠存儲44字節。
因此 embstr 形式,能夠存儲最大字符串長度是44字節。
Strings Strings are the most basic kind of Redis value. Redis Strings are binary safe, this means that a Redis string can contain any kind of data, for instance a JPEG image or a serialized Ruby object. A String value can be at max 512 Megabytes in length.
SET q sc
encoding:embstr,長度爲3
如今作追加操做,APPEND q scadd ,encoding:raw,長度8
爲何從 sc ----> scscadd 簡單的追加操做內部類型會從 embstr -----> raw ,如何解釋?
喜歡的歡迎加公衆號或者留言評論探討