1、Redis为了节约内存空间,zset和hash在对象比较少的时候,采用压缩列表(ziplist)来存储,可以用过debug object key 来查看结构
1)ziplist的结构体
struct ziplist int32 zlbytes; // 整个压缩列表占用字节数 int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点 int16 zllength; // 元素个数 T[] entries; // 元素内容列表,挨个挨个紧凑存储 int8 zlend; // 标志压缩列表的结束,值恒为 0xFF } |
各字段排列如下图所示:
解释:压缩列表支持双向遍历,ztail_offset字段是为了快速定位到最后一个元素,然后倒着遍历
2)entry结构体
struct entry { int prevlen; // 前一个 entry 的字节长度 int encoding; // 元素类型编码 optional byte[] content; // 元素内容 } |
解释:prevlen字段是为了从后往前遍历的时候,通过这个字段快速定位到下个元素的位置,该字段是一个变长的整数,当字符串长度小于254时,使用一个字节表示;但大于254的时候采用5个字节表示,其中第一个字节时0xFF(254),剩下四个表示长度。
encoding:元素内容存储编码格式
2、增加元素
由于ziplist是紧凑存储,没有多余的空间来添加元素,所以没次添加的时候,系统都要进行扩容,根据内存分配器算法和当前ziplist内存大小,reallloc是重新分配新的内存空间或者在原有地址上进行扩展,如果真心爱在原有地址上扩展就不需要进行旧内容的内存拷贝。
3、级联更新
源码如下:
unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) { size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize; size_t offset, noffset, extra; unsigned char *np; zlentry cur, next; while (p[0] != ZIP_END) { zipEntry(p, &cur); rawlen = cur.headersize + cur.len; rawlensize = zipStorePrevEntryLength(NULL,rawlen); /* Abort if there is no next entry. */ if (p[rawlen] == ZIP_END) break; zipEntry(p+rawlen, &next); /* Abort when "prevlen" has not changed. */ // prevlen 的长度没有变,中断级联更新 if (next.prevrawlen == rawlen) break; if (next.prevrawlensize < rawlensize) { /* The "prevlen" field of "next" needs more bytes to hold * the raw length of "cur". */ // 级联扩展 offset = p-zl; extra = rawlensize-next.prevrawlensize; // 扩大内存 zl = ziplistResize(zl,curlen+extra); p = zl+offset; /* Current pointer and offset for next element. */ np = p+rawlen; noffset = np-zl; /* Update tail offset when next element is not the tail element. */ // 更新 zltail_offset 指针 if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) { ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra); } /* Move the tail to the back. */ // 移动内存 memmove(np+rawlensize, np+next.prevrawlensize, curlen-noffset-next.prevrawlensize-1); zipStorePrevEntryLength(np,rawlen); /* Advance the cursor */ p += rawlen; curlen += extra; } else { if (next.prevrawlensize > rawlensize) { /* This would result in shrinking, which we want to avoid. * So, set "rawlen" in the available bytes. */ // 级联收缩,不过这里可以不用收缩了,因为 5 个字节也是可以存储 1 个字节的内容的 // 虽然有点浪费,但是级联更新实在是太可怕了,所以浪费就浪费吧 zipStorePrevEntryLengthLarge(p+rawlen,rawlen); } else { // 大小没变,改个长度值就完事了 zipStorePrevEntryLength(p+rawlen,rawlen); } /* Stop here, as the raw length of "next" has not changed. */ break; } } return zl; } |
解释:
1)每个entry都有一个prevlen字段,而且是变长的,以253为零界点,大于253用5个字节表示,小于或者等于用1个字节,如果由253变成了254,则会发生级联更新
2)如果每个entry恰好都存储253个字节内容,那么修改第一个entry内容旧会导致后续所有的entry发生级联更新,还是比较耗费计算机资源的。
3)删除中间节点也会发生级联更新
4、IntSet小整数集合
当set集合容纳的元素都是整数并且个数较少时,Redis会使用intSet来存储元素,intSet是紧凑数组,支持16、32、64位
1)结构体
struct intset int32 encoding; // 决定整数位宽是 16 位、32 位还是 64 位 int32 length; // 元素个数 int } |
如果存储的是非整数,立马会更改类型,如下图所示
> sadd codehole 1 2 3 (integer) 3 > debug object codehole Value at:0x7fec2dc2bde0 refcount:1 encoding:intset serializedlength:15 lru:6065795 lru_seconds_idle:4 > sadd codehole go java python (integer) 3 > debug object codehole Value at:0x7fec2dc2bde0 refcount:1 encoding:hashtable serializedlength:22 lru:6065810 lru_seconds_idle:5 |