簡介
Redis zipmap是一個String -> String Map data structure optimized for size。不要被它的名字所迷惑,它實際上沒有做壓縮操作。是一個連續(xù)的key value集合。
它的特點就是維護map需要的額外信息很少。最少可以到達只需要三個字節(jié)。不過這樣做的后果就是時間復雜度為o(N),需要便利才能操作。所以相對來說在定義上ZIPMAP_BIGLEN 就只有254(不過并非到達254就不給你存了,它還是會存的).
數(shù)據(jù)結構
zipmap的數(shù)據(jù)結構特別簡單,簡單到兩個字節(jié),首字節(jié)保存長度,尾字節(jié)保存結尾標志ZIPMAP_BIGLEN。ZIPMAP_BIGLEN=254
創(chuàng)建zipmap
unsigned char *zipmapNew(void) {
unsigned char *zm = zmalloc(2);
zm[0] = 0; /* Length */
zm[1] = ZIPMAP_END;
return zm;
}
對就是這么簡單。分配兩字節(jié)的空間設置首字節(jié)為長度0,尾字節(jié)為結束標識。
數(shù)據(jù)編碼
在說插入之前我們先來看一下,對于一個key->value的數(shù)據(jù),zipmap是怎么保存的。
引用源碼中的介紹
Memory layout of a zipmap, for the map "foo" => "bar", "hello" => "world":
<zmlen><len>"foo"<len><free>"bar"<len>"hello"<len><free>"world"
- zmlen 這個是我們的首字節(jié)保存key value的對數(shù)。不過zmlen <=ZIPMAP_BIGLEN大了他就不表示了。
- len 表示保存的字符串的長度。第一個len是key的長度。第二個len是value字符串的長度。len在小于254的時候使用一個字節(jié)表示。這個字節(jié)就是保存len的長度。當len大于254的時候。這時候使用sizeof(unsigned int)+1來保存。其中第一個字節(jié)設置為ZIPMAP_BIGLEN。后四個字節(jié)用來實際存儲長度。
- free 一個字節(jié),用在修改key對應的value后表示剩余的空間。發(fā)生在新的value比原來的value的len長度要小的時候。受到ZIPMAP_VALUE_MAX_FREE的限制。當大于ZIPMAP_VALUE_MAX_FREE的時候觸發(fā)resize
- keyvalue鍵值對直接沒有對于的數(shù)據(jù),直接連接
len的編碼
因為我們的len有著兩種保存方式。所以先來看下len相關的編碼和解碼
/*獲取保存len需要的長度 當len<ZIPMAP_BIGLEN時為1,len>ZIPMAP_BIGLEN時為sizeof(unsigned int)+1)*/
#define ZIPMAP_LEN_BYTES(_l) (((_l) < ZIPMAP_BIGLEN) ? 1 : sizeof(unsigned int)+1)
/*對len進行編碼,當沒有傳入p時,返回需要的空間大小,當p存在時編碼保存len并且返回空間大小*/
static unsigned int zipmapEncodeLength(unsigned char *p, unsigned int len) {
if (p == NULL) {
return ZIPMAP_LEN_BYTES(len);//為空直接返回大小
} else {
if (len < ZIPMAP_BIGLEN) {
p[0] = len;//當len<ZIPMAP_BIGLEN時直接保存
return 1;
} else {
p[0] = ZIPMAP_BIGLEN;//首字節(jié)設置為ZIPMAP_BIGLEN作為標記
memcpy(p+1,&len,sizeof(len));
memrev32ifbe(p+1);//對于大端小斷的操作
return 1+sizeof(len);
}
}
}
/*解len長度*/
static unsigned int zipmapDecodeLength(unsigned char *p) {
unsigned int len = *p;
if (len < ZIPMAP_BIGLEN) return len;//不是ZIPMAP_BIGLEN標記直接返回長度
memcpy(&len,p+1,sizeof(unsigned int));
memrev32ifbe(&len);//大端小端
return len;
}
key value編碼
對于key的編碼保存方式的<len>key
對于value的編碼是<len><free>value
對于一個key->value的鍵值對的編碼則是<len>key<len><free>value,他們中間沒有間隔直接連接起來。
我們獲取一個key-value所需要的總長度
static unsigned long zipmapRequiredLength(unsigned int klen, unsigned int vlen) {
unsigned int l;
l = klen+vlen+3;// 3 = klen vlen free(一般是klen vlen一般是1)
if (klen >= ZIPMAP_BIGLEN) l += 4;
if (vlen >= ZIPMAP_BIGLEN) l += 4;
return l;
}
key value的解碼
對于key,先獲得klen 然后獲得保存klen所需要的長度klenlen,再偏移klenlen的長度得到保存的key值
unsigned char * p;//表示保存key的開始
unsigned int klen = zipmapDecodeLength(p);
unsigned int klenlen = zipmapEncodeLength(NULL,klen);
unsigned char* key = p+klenlen;
對于value來說基本和key一致,因為在value實際保存位置前,還有一個一字節(jié)的free的空間,所以我們需要多偏移一個字節(jié)。
unsigned char * p;//表示保存value的開始
unsigned int vlen = zipmapDecodeLength(p);
unsigned int vlenlen = zipmapEncodeLength(NULL,vlen);
unsigned char* key = p+vlenlen+1;
搜索
我們知道編碼和保存方式之后,來看下我們的查找操作
static unsigned char *zipmapLookupRaw(unsigned char *zm, unsigned char *key, unsigned int klen, unsigned int *totlen) {
/*1*/ unsigned char *p = zm+1, *k = NULL;//zm實際保存的數(shù)據(jù)從第二個字節(jié)開始
unsigned int l,llen;
while(*p != ZIPMAP_END) {//沒有結束
unsigned char free;
/* Match or skip the key */
/*2*/ l = zipmapDecodeLength(p);//長度打頭
llen = zipmapEncodeLength(NULL,l);//返回length的長度
/*3*/ if (key != NULL && k == NULL && l == klen && !memcmp(p+llen,key,l)) {//k為null是才比較不為null代表找到
/* Only return when the user doesn't care
* for the total length of the zipmap. */
if (totlen != NULL) {//返回長度不
k = p;
} else {
return p;//直接返回
}
}
p += llen+l; //偏移數(shù)據(jù)的長度和保存長度的長度
/* Skip the value as well */
/*4*/l = zipmapDecodeLength(p);//value的長度
p += zipmapEncodeLength(NULL,l);//保持value長度的長度
free = p[0];
p += l+1+free; /* +1 to skip the free byte */
}
if (totlen != NULL) *totlen = (unsigned int)(p-zm)+1;
return k;
}
對于我們的搜索key來說特別簡單。
1.我們先對傳入的zm進行1的偏移,因為第二個字節(jié)才是保存的第一個key的開始。
2.獲得key的值
3.對key進行比對,成功代表找到,沒成功進入偏移掉key的長度,進入到4
4.偏移一個value的長度,value偏移的時候需要注意不要忘記free保存的空閑長度和free本身的一字節(jié)的長度。
最后如果傳入了totlen則代表需要獲取保存所有數(shù)據(jù)的長度
插入
unsigned char *zipmapSet(unsigned char *zm, unsigned char *key, unsigned int klen, unsigned char *val, unsigned int vlen, int *update) {
unsigned int zmlen, offset;
unsigned int freelen, reqlen = zipmapRequiredLength(klen,vlen);//獲取key value保存需要的總長度
unsigned int empty, vempty;
unsigned char *p;
freelen = reqlen;
if (update) *update = 0;
/*1*/p = zipmapLookupRaw(zm,key,klen,&zmlen);//查找key是否存在并且返回總長度
if (p == NULL) {//代表沒有
/* Key not found: enlarge */
/*2*/zm = zipmapResize(zm, zmlen+reqlen);//重新分配
p = zm+zmlen-1;
zmlen = zmlen+reqlen;
/* Increase zipmap length (this is an insert) */
if (zm[0] < ZIPMAP_BIGLEN) zm[0]++;
} else {
/* Key found. Is there enough space for the new value? */
/* Compute the total length: */
if (update) *update = 1;
freelen = zipmapRawEntryLength(p);//獲取原來保存key value的總大小
if (freelen < reqlen) {//總大小不夠
/* Store the offset of this key within the current zipmap, so
* it can be resized. Then, move the tail backwards so this
* pair fits at the current position. */
/*3*/ offset = p-zm;//原來保存key value的偏移
zm = zipmapResize(zm, zmlen-freelen+reqlen);//重新分配空間
p = zm+offset;//偏移到原來保存key value的位置
/* The +1 in the number of bytes to be moved is caused by the
* end-of-zipmap byte. Note: the *original* zmlen is used. */
memmove(p+reqlen, p+freelen,zmlen - offset -freelen -1 );//內存移動過來
/*
我們需要移動原來的keyvlaue后的所有數(shù)據(jù)(排除最后一個 在resize中已經設置)到新的keyvalue的結束位置
p = zm +offset 所以這個時候p是保存這個key value開始的位置
reqlen新key value保存需要的總大小 所以p+reqlen 就到達下個key value的起始位置(p+reqlen-1 為新key value的結束位置)
freelen 保存原來key value的總大小p+freelen 同里就達到了原來下一個key value保存的位置
這個時候我們把zm看成一個純的字符串,來看內存移動
zmlen為字符串的長度,p+reqlen為需要移動到的起始位置,p+freelen為需要移動的起始位置
offset為zm到原來字符串中間數(shù)據(jù)的長度 freelen 為原來kevalue的長度
zmlen - offset -freelen 就等于剩下數(shù)據(jù)的長度 -1是因為最后一個數(shù)據(jù)已經設置不需要移動
*/
zmlen = zmlen-freelen+reqlen;//獲得新大小
freelen = reqlen;//這時候沒有剩余空間
}
}
/* We now have a suitable block where the key/value entry can
* be written. If there is too much free space, move the tail
* of the zipmap a few bytes to the front and shrink the zipmap,
* as we want zipmaps to be very space efficient. */
empty = freelen-reqlen;
if (empty >= ZIPMAP_VALUE_MAX_FREE) {//如果剩的太多了
/* First, move the tail <empty> bytes to the front, then resize
* the zipmap to be <empty> bytes smaller. */
/*4*/offset = p-zm;
memmove(p+reqlen, p+freelen, zmlen-(offset+freelen+1));//回收內存
/*
內存前移和后移一樣的 resize會設置最后一個
*/
zmlen -= empty;//變小
zm = zipmapResize(zm, zmlen);
p = zm+offset;//
vempty = 0;
} else {
vempty = empty;
}
/* Just write the key + value and we are done. */
/* Key: */
/*5*/
p += zipmapEncodeLength(p,klen);//設置并返回
memcpy(p,key,klen);//拷貝key
p += klen;
/* Value: */
p += zipmapEncodeLength(p,vlen);
*p++ = vempty;//設置free
memcpy(p,val,vlen);
return zm;
}
設置一個key value經歷了以下步驟
- 首先對key進行搜索操作同時返回zmlen
2.如果沒有搜索到,那么直接分配一個keyvalue所需要的空間
3.如果搜索到原來有,那么判斷當前需要的空間和原來空間的大小,如果當前空間比原來空間小,那么就需要再次分配一個大的內存空間,然后把p+freelen后的數(shù)據(jù)移動到p+reqlen,不過需要注意的是在代碼中,最末尾的標記是在resize的時候設置了,所以在移動的時候少了一個移動
4.當原來的空間比需要的空間大,并且當freelen-reqlen的時候大于了允許的最大freesize,那么就需要重新分配一個小的空間,也是把p+freelen后的數(shù)據(jù)移動到p+reqlen,不過需要注意的是在代碼中,最末尾的標記是在resize的時候設置了,所以在移動的時候少了一個移動
5.最后把新的keyvalue編碼到新的空間。
刪除
unsigned char *zipmapDel(unsigned char *zm, unsigned char *key, unsigned int klen, int *deleted) {
unsigned int zmlen, freelen;
unsigned char *p = zipmapLookupRaw(zm,key,klen,&zmlen);//找到
if (p) {
freelen = zipmapRawEntryLength(p);//獲取這個keyvalue總長度
memmove(p, p+freelen, zmlen-((p-zm)+freelen+1));//回收內存 也注意最后一位
zm = zipmapResize(zm, zmlen-freelen);
/* Decrease zipmap length */
if (zm[0] < ZIPMAP_BIGLEN) zm[0]--;
if (deleted) *deleted = 1;
} else {
if (deleted) *deleted = 0;
}
return zm;
}
刪除相對來說就是特別的簡單了
如果找到key那么或取這個keyvalue的空間,前移keyvalue后面的數(shù)據(jù),更改大小
總結
zipmap的數(shù)據(jù)結構相對簡單。我們只需要理解了keyvalue的編碼以后,就能快速的掌握。在插入和刪除的時候牽涉到的內存移動,都要注意在源碼中沒有移動最后一位。因為是在resize中設置了。
使用的地方
The Redis Hash type uses this data structure for hashes composed of a small number of elements, to switch to a hash table once a given number ofelements is reached.
它使用在當hashtable還特別小的時候。因為hashtable需要編碼的額外信息很大 sizeof(struct dictEntry) = 24,咱們的zipmap最少只需要三個。最多11個。
在查詢效率上當數(shù)據(jù)量特別小的時候順序查詢花費的時間成本雖然是o(N),但是N小,所以是可以接受的。這樣可以節(jié)約出內存。
我們以后還會看到很多這種節(jié)約內存的做法