對於上一篇文章,我又自己總結歸納並補充了一下,有了第二篇。
概覽
<<左移
開始之前,我們先准備點東西:位運算
i<<n 總結為 i*2^n
所以
1<<5 = 2^5
1<<8 = 2^8
1<<16 = 2^16
1<<32 = 2^32
1<<64 = 2^64
SDS 5種數據類型
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個數據類型的命名規則。
外部類型String 找 SDS結構
我們現在有定義了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結構,現在到了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和 內部類型的關系類似於這樣的:
連續內存,和非連續內存
44 字節
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頭信息16字節,再減去3字節SDS頭信息,剩下45字節,再去除\0結尾。這樣最后可以存儲44字節。
所以 embstr 形式,可以存儲最大字符串長度是44字節。
關於字符串最大是512M
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.
出個題(redis 5.0.5版本)
SET q sc
encoding:embstr,長度為3
現在做追加操作,APPEND q scadd ,encoding:raw,長度8
為什么從 sc ----> scscadd 簡單的追加操作內部類型會從 embstr -----> raw ,如何解釋?
喜歡的歡迎加公眾號或者留言評論探討