一起学习Redis--压缩列表

933 阅读21分钟

前言

​ 前面一起学习Redis--怎么计算字符串内存空间的占用文章中,介绍了字符串在Redis中的数据结构形态(SDS),以及Redis是怎样组织Key和Value之间的关系,以及对字符串的一些编码规则,来进行内存的优化。共涉及DictEntry、RedisObject、SDS 3种结构,字符串编码、对象共享机制,以及jemalloc 内存分配。Redis一直在寻找时间、空间的一种均衡,这种思想体现在丰富的数据结构模型上。

数据类型以及数据结构的关系

​ Redis提供了丰富的数据类型,有字符串、List、Set、Sorted Set、Hash,以供开发者在不同的场景下使用。这些数据类型底层在不同的情况下,有不同的数据结构提供支持,具体如下图所示

数据类型与数据结构之间的关系

​ 从图中看到,除了String类型对应一种数据结构动态字符串(SDS),其它的4中类型都有2种数据结构进行支持,其中压缩列表ZipList支持的数据类型最为丰富,今天我们就先围绕着ZipList进行分析他存在的意义,以及他的在服务于上层数据类型时的工作原理。本篇的关键词:字节数组、长度编码

ZipList数据模型

​ 压缩列表本质上,就是一组连续的字节数组,在模型上,将这些连续的数组分为3大部分,分别是header、entry集合、end,其中header由zlbytes+zltail+zllen组成,zlend是一个单字节255(1111 1111),用做ZipList的结尾标识符,如图所示

ZipList数据模型

zlbytes:  压缩列表占用的字节数,用4个字节表示
zltail:   从压缩列表指针,到tail节点指针之间的字节数(位移),用4个字节表示
zllen:		entry集合的数量,用2个字节表示
zlend:		压缩列表结束标识符,用1个字节表示
ZIPLIST_ENTRY_HEAD:	Entry集合第一个节点的指针
ZIPLIST_ENTRY_TAIL: Entry集合最后一个节点的指针
ZIPLIST_ENTRY_END:	ziplist 末端结束标识符的指针

从结构上可以看出,压缩列表最大可以支持2^32字节大小的数据,但entry个数是最大只能记录2^16个,因此如果执行获取元素个数的操作,当压缩列表entry个数超过2^16个,那么就需要遍历entry字节数组了,此时的时间复杂度为O(N)

Entry的结构模型

​ entry是一个个数据节点,由 prevrawlen、len、value 三部分组成,分别是前置节点占用的字节数、当前节点value占用的字节数、value存储的是数据。

Entry字节数组结构图

由于zlend 采用255(1111 1111)作为压缩列表的结尾标识,因此每个entry的首字节即prevrawlen首字节是不能出现255数值的,Redis对prevrawlen 进行了编码,254 代表着需要4个字节来标识前置节点的长度;

prevrawlen 字节编码

0-253:一个字节表示即可
>=254:需要占用5个字节,第一个字节值为254,剩余的4个字节来表示前置节点的占用字节数

len标识value需要多少个字节,相比于prevrawlen,len就相对复杂些,len也有字节编码,Redis 通过len 的首字节进行编码encoding,从类型上分为字符型和数值型两种编码。

len 字节编码 encoding

如果value是数值型
encoding=11100000:标识着value是一个需要用64bit表示的数字,即需要8字节
encoding=11010000:标识着value是一个需要用32bit表示的数字,即需要4字节
encoding=11110000:标识着value是一个需要用24bit表示的数字,即需要3字节
encoding=11000000:标识着value是一个需要用16bit表示的数字,即需要2字节
encoding=11111110:标识着value是一个需要用 8bit表示的数字,即需要1字节
当然redis还有一些优化,就是用11110000<encoding< 11111110 来表示0~12的数字

如果value是字符型,encoding< 11000000 ,因为数值型的编码范围在>=11000000范围内
encoding<=00111111:encoding即len值,即当value长度小于64字节时,len用一个字节表示
10000000>encoding>=01000000:encoding高1位为0,高2位为1时,标识len需要两个字节表示,																						 假设byte1,byte2分别代表两个字节的值;value的长度等(byte1-																				 64)*256+byte2
encoding=10000000:标识着len占用5个字节,后4个字节来表示value的字节数

图解字节数组的变化

下面通过操作List类型的数据,来看看压缩列表编码机制

执行lpush list 1

lpush 1

Prevrawlen:因为没有前置节点,因此前置节点的字节数是0,由于小于254 ,用一个字节表示

len:由于1是数值型,所以才有数值型编码,且在[0~12]范围内,所以用一个字节表示,且encoding也标识对应的值。11110001 = 0 ,11110010 =1 .....

执行lpush list abcd

lpush abcd

value=abcd节点(1+1+4=6byte):

Prevrawlen:因为没有前置节点,因此前置节点的字节数是0,由于小于254 ,用一个字节表示

len:由于abcd是字符串,且value字节数=4 小于 64,因此一个字节表示

value=1 的节点:

Prevrawlen:前置节点存在,且占用6字节,小于254,因此prevrawlen 用一个字节表示

执行lpush list (10abcd...xyz)

lpush 260

value= 10*abcd...xyz(共占用26X10=260字节)

Prevrawlen:因为没有前置节点,因此前置节点的字节数是0,由于小于254 ,用一个字节表示

len:由于value占用260字节,大于63(00111111),小于16383(00111111 11111111),所以占用两个字节,首字节以01000000 即64为起始值,每当第二个节点产生进位,01000000+1,如图首节点65(01000001),第二个节点00000100即4,套用公式,len=(65-64)*256+4 = 260.

value= abcd节点

Prevrawlen:由于前置节点占用的字节数263>254,因此prevrawlen字节数有原来的1个字节调整为5个字节,同样触发级联更新value=1的节点的prevrawlen的值,由原来的6变为10。prevrawlen 后四位字节代表263数值,看着有些奇怪,主要是redis这4个字节进行了大小端转换,转成我们日常理解的就是00000000|00000000|00000001|00000111 这样就是我们比较熟悉的263了。

源码解读

前面我们了解到ZipList结构,以及前后节点之间的关系,编码模型,下面我们通过部分源码+伪代码的形式,了解下ZipList 提供了那些常见的函数

ziplistNew (void)

新建一个空的压缩列表

unsigned char *ziplistNew(void) {

    // ZIPLIST_HEADER_SIZE 是 ziplist 表头的大小,即zlbytes+zltail+zllen=4+4+2=10
    // 1 字节是表末端 ZIP_END 的大小
    unsigned int bytes = ZIPLIST_HEADER_SIZE+1;

    // 为表头和表末端分配空间,bytes = 11,默认采用jemalloc内存分配器,因此实际申请的内存空间为16byte
    unsigned char *zl = zmalloc(bytes);

    // 初始化表属性,设置zlbytes 的值=11
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
  	//设置zltail的值=10
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    //设置zllen的值=0
    ZIPLIST_LENGTH(zl) = 0;

    // 设置表末端字节的值=255
    zl[bytes-1] = ZIP_END;

    return zl;
}

ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where)

​ 往压缩列表zl指定位置where添加添加一个长度为slen的字符串,where的其实只是表示在列表头部添加还是尾部添加。需要留意的是,该函数在某些场景下时间复杂度是O(N^2),场景是如果所有entry节点的长度都是250~253时,当头部插入一个大于entry长度大于等于254的节点时,后置节点的prevrawlen都会由原来的1个字节扩展至5个字节,导致后置节点的长度相应的增加4,继而触发后续的节点级联更新。

//T=O(N^2)
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {

    // 根据 where 参数的值,决定将值推入到表头还是表尾,持有相应位置的指针
    unsigned char *p;
    p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);

    // 返回添加新值后的 ziplist
    // T = O(N^2)
    return __ziplistInsert(zl,p,s,slen);
}

/**
* 在zl压缩列表的p的位置,新增一个长度为slen的,value为s的字符串
**/
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    // 记录当前 ziplist 的长度
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen, prevlen = 0;
    size_t offset;
    int nextdiff = 0;
    unsigned char encoding = 0;
    long long value = 123456789; /* initialized to avoid warning. Using a value
                                    that is easy to see if for some reason
                                    we use it uninitialized. */
    zlentry entry, tail;
		//根据p的位置,计算前置节点的长度,如果在头部,那么prelen=0,如果在队尾且列表不为空,那么prelen=前置节点的长度,否则列表为空,prelen=0;
    prelen=getPrelen();

    //计算value的len
  	//此处会尝试进行数值编码转换
  	reqlen=getValueLen();
    /* We need space for both the length of the previous entry and
     * the length of the payload. */
    // 计算前置节点编码所需的字节大小,即prevrawlen所占用的字节数
    // T = O(1)
    reqlen += zipPrevEncodeLength(NULL,prevlen);
    // 计算编码所需的字节大小,即len所占用的字节数
    // T = O(1)
    reqlen += zipEncodeLength(NULL,encoding,slen);
  	//截止目前,新节点的所需字节数已计算完毕,值为reqlen

    /* When the insert position is not equal to the tail, we need to
     * make sure that the next entry can hold this entry's length in
     * its prevlen field. */
    // 只要新节点不是被添加到列表末端,
    // 那么程序就需要计算新节点的后置节点的prevrawlen占用字节数是否变大,即假如之前后置节点prevrawlen占用		//1个字节,现在新节点占用字节数大于253,那么后置节点prevrawlen所占用的字节数就由1变为5,nextdiff=4
    // nextdiff 保存了新旧编码之间的字节大小差,如果这个值大于 0 
    // 那么说明需要对 p 所指向的节点(的 header )进行扩展
    // T = O(1)
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;

    /* Store offset because a realloc may change the address of zl. */
    // 因为重分配空间可能会改变 zl 的地址
    // 所以在分配之前,需要记录 zl 到 p 的偏移量,然后在分配之后依靠偏移量还原 p 
    offset = p-zl;
  	// 调整压缩列表的空间,并更新zlbytes的值
    // curlen 是 ziplist 原来的长度
    // reqlen 是整个新节点的长度
    // nextdiff 是新节点的后继节点扩展 header 的长度(要么 0 字节,要么 4 个字节)
    // T = O(N)
    zl = ziplistResize(zl,curlen+reqlen+nextdiff);
    p = zl+offset;

    /* Apply memory move when necessary and update tail offset. */
    if (p[0] != ZIP_END) {
        // 新元素之后还有节点,因为新元素的加入,需要对这些原有节点进行调整

        /* Subtract one because of the ZIP_END bytes */
        // 移动现有元素,为新元素的插入空间腾出位置
        // T = O(N)
      	//memmove(void *dst, const void *src, size_t len)
      	//q+prelen,即移动到目的地的起始位置
      	//p-nextdiff,从哪个地方开始拷贝
      	//curlen-offset-1+nextdiff,需要移动的字节数,即*P后面的字节,除了ZIP_END都需要移动
      	//注意此处只是新插入节点的后置节点计算了nextdiff,后续的其它节点尚未计算。
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);

        /* Encode this entry's raw length in the next entry. */
        // 上面移动完,现在更新下新节点的后置节点的prevrawlen数值。
        // p+reqlen 定位到后置节点
        // reqlen 是新节点的长度
        // T = O(1)
        zipPrevEncodeLength(p+reqlen,reqlen);

        /* Update offset for tail */
        // 更新到达表尾的偏移量,将新节点的长度也算上,这种算法,只适用于新节点后面只有一个后置节点
        ZIPLIST_TAIL_OFFSET(zl) =
            intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);

        /* When the tail contains more than one entry, we need to take
         * "nextdiff" in account as well. Otherwise, a change in the
         * size of prevlen doesn't have an effect on the *tail* offset. */
        // 如果新节点的后面有多于一个节点
        // 那么程序需要将 nextdiff 记录的字节数也计算到表尾偏移量中
        // 这样才能让表尾偏移量正确对齐表尾节点
        // T = O(1)
        tail = zipEntry(p+reqlen);//获取新节点的后置节点
      	//tail.headersize = tail.prevrawlen+编码len的字节数,tail.len=value的字节数
      	//如果新节点后面不止一个后继节点,那么表尾偏移量在后移nextdiff。
        if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
        }
    } else {
        /* This element will be the new tail. */
        // 新元素是新的表尾节点
        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
    }

    /* When nextdiff != 0, the raw length of the next entry has changed, so
     * we need to cascade the update throughout the ziplist */
    // 当 nextdiff != 0 时,新节点的后继节点的(header 部分)长度已经被改变,
    // 所以需要级联地更新后续的节点
    if (nextdiff != 0) {
      	//级联更新,可能会重新分配zl地址,那么需要先计算出*p的偏移量
        offset = p-zl;
        // T  = O(N^2)
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }

    /* Write the entry */
    // 一切搞定,将前置节点的长度写入新节点的 header
    p += zipPrevEncodeLength(p,prevlen);
    // 将节点值的长度写入新节点的 header
    p += zipEncodeLength(p,encoding,slen);
    // 写入节点值
    if (ZIP_IS_STR(encoding)) {
        // T = O(N)
        memcpy(p,s,slen);
    } else {
        // T = O(1)
        zipSaveInteger(p,value,encoding);
    }

    // 更新列表的节点数量计数器
    // T = O(1)
    ZIPLIST_INCR_LENGTH(zl,1);

    return zl;
}

从源码中可以看出,新增一个节点,分一下几个步骤

  1. 确定新节点的插入位置,表头或者表尾
  2. 数据准备,计算新节点的前置节点长度prelen,新节点的占用字节数 reqlen。
  3. 计算新节点的第一个后置节点的prevrawlen是否需要申请额外的字节来表示新节点的字节数nextdiff。
  4. 调整zl压缩列表的长度,更新zl压缩列表的zlbytes值
  5. 移动复制原有的数据节点,为新节点腾出空间
  6. 更新新节点的第一个后继节点的prevrawlen的值。
  7. 更新zl压缩列表的zltail的值
  8. 如果新节点的第一个后继节点的prevrawlen占用的字节数有调整,则尝试级联更新后继节点的prevrawlen。
  9. 给新节点设置prevrawlen、len、value的值
  10. 更新zl压缩列表的zllen的值。

ziplistIndex(unsigned char *zl, int index)

​ 返回指针P,当index 小于0 则从表尾寻址,否则从表头寻址,例如index = -1,那么p指向tail尾结点,如果index=-2 ,那么p指向tail的前置节点。

ziplistNext(unsigned char *zl, unsigned char *p)

​ 返回P指向节点的后置节点,如果p是tail节点,则返回NULL

ziplistPrev(unsigned char *zl, unsigned char *p)

​ 返回P指向节点的前置节点,如果P是头节点,则返回NULL

ziplistGet(unsigned char *p, unsigned char *sval, unsigned int *slen, long long *lval)

​ 获取P指向节点的值,如果是字符串,则将value 赋值给sval,value的长度赋值给slen,如果是数值型,则将value赋值给lval

ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen)

​ 将长度为slen的包含给定值s的新节点,放置到P的位置,如果P指向一个节点,那么新节点放置在原有节点的前面。

ziplistDelete(unsigned char *zl, unsigned char *p)

​ 删除P指向的节点,可能会触发级联更新

ziplistDeleteRange(unsigned char *zl, unsigned int index, unsigned int num)

​ 首先通过ziplistIndex定位P指向的节点,然后连续向后连续删除num个节点。可以联想下list.trim操作

ziplistCompare(unsigned char *p, unsigned char *s, unsigned int slen)

​ 比较P指向的节点值与给定的s值,如果相等,则返回1,否则返回0

ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip)

​ 从P指向的节点开始,以skip为步长,查找与给定的vstr值相同的首个节点,主要应用于hash数据类型的filed查找

ziplistLen(unsigned char *zl)

​ 返回压缩列表的节点个数

ziplistBlobLen(unsigned char *zl)

​ 返回压缩列表占用的内存字节数

几种数据类型在压缩列表中的使用形态

List数据类型

​ List数据类型的元素无需去重,但需要有序,压缩列表本质的数据模型就是一个个连续的entry数组,因此每个entry节点就代表着一个集合元素,相对理解简单

Sorted Set & Hash数据类型

​ 相比于list,sorted set 包含member 和 score,Hash包含field 和 value,因此可以将member、score、field、value 各个看成entry,比如entry数组中偶数index(包含0) 代表着 member 或者filed,奇数index代表着score或者value。这样查找数据节点时,ziplistFind 通过skip跳过score或者value,然后对比member、filed进行集合意义上的滤重。唯一不同的事,sorted set 是按照score排序的,因此在执行zadd的时候,如果找到对应的member节点后,还要获取score,如果score!=oldScore,直接删除member 和score节点,执行插入操作。而Hash数据类型只需要找到对应的filed节点,更新下value即可。

其它对应的数据结构

双端链表 和 Hash表在Redis中,是怎样的一种结构呢?我们可以通过下图简单了解下,在Redis中的一种组合形态

全局形态

从图中可以看出,我们可以通过key的计算,算出对应的slot,然后找到对应的dictEntry,dictEntry->val 指向了一个redisObject对象,而redisObject->ptr指向了各类型的数据结构。

List数据结构

/*
 * 双端链表结构
 */
typedef struct list {

    // 表头节点
    listNode *head;

    // 表尾节点
    listNode *tail;

    // 节点值复制函数,如果不指定相应的复制函数时,所谓的复制是浅复制,即指针指向同一个元素对象
    void *(*dup)(void *ptr);

    // 节点值释放函数,如果不指定释放函数,则使用zfree释放
    void (*free)(void *ptr);

    // 节点值对比函数,如果不指定对比函数,就直接进行地址对比,类似于java中字符串的==
    int (*match)(void *ptr, void *key);

    // 链表所包含的节点数量
    unsigned long len;

} list;

ListNode数据结构

/*
 * 双端链表节点
 */
typedef struct listNode {

    // 前置节点
    struct listNode *prev;

    // 后置节点
    struct listNode *next;

    // 节点的值,此处指针指向的是一个redisObject
    void *value;

} listNode;

Hash数据结构

/*
 * 字典
 */
typedef struct dict {

    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */

} dict;

dictEntry数据结构

/*
 * 哈希表节点
 */
typedef struct dictEntry {
    
    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;

} dictEntry;

压缩列表存在的意义

​ Redis主要操作对象是内存,对外提供高效的读写,因此涉及到时间、空间两个维度。例如Redis通过dict字典来进行key索引存储,用O(1)时间复杂度来支持key的查询,也会通过rehash来拓展字典的大小,降低hash冲突带来的链式寻址的可能性。从一起学习Redis--怎么计算字符串内存空间的占用文章中其实可以看出,如果我们要想存储一个小的数据时,可能redis自身的数据结构占用的内存空间,会远远的大于数据本身使用的空间。虽然压缩列表的查询操作、插入操作的时间复杂度不是O(1),甚至插入操作会触发O(N^2)的时间复杂度,但是当节点数量zllen较少以及占用字节数zlbyte较小时,通过cpu缓存的特性,性能并不会太低,这也是时间和空间的一种权衡。

​ 从上面List和Hash的数据结构我们可以看出,当要存储数据时,会有额外的指针、属性带来的内存开销,而压缩列表只是单纯的字节数组来进行值存储即可,我们可以通过例子来感知下;假如我们要为list的存储一个字符串,抛开带来的级联更新因素,假如字符串占用x字节,那么需要最少prevrawlen(1)+len(1)+x=2+x,最多需要prevrawlen(5)+len(5)+x = 10+x 字节;然而使用linkedList双端链表,需要prev(8)+next(8)+value=16+value,由于value是redisObject类型,我们从一起学习Redis--怎么计算字符串内存空间的占用这篇博文中知道,redisObject需要(4+4+24)/8+4+8 = 16字节,其中ptr指向了SDS字符串,而SDS需要的空间大小为9+len+free,其中len=x,而listNode需要8+8+8=24->32字节,即总共需要的空间大小为32+16+9+x+free,新建的字符串free=0,需要空间大小需要48+(9+x)[此处根据x大小不同申请的内存空间也不一样,参考jemalloc内存分配机制],如果x非常小,那么额外的数据开销将远远大于ZipList。

下面通过对List数据类型的操作来验证下空间的消耗
127.0.0.1:6379> lpush list a      //创建一个压缩列表
(integer) 1
127.0.0.1:6379> object encoding list
"ziplist"  //压缩列表
127.0.0.1:6379> info memory
# Memory
used_memory:1022656	//此时的内存占用大小
used_memory_human:998.69K
used_memory_rss:1015808
used_memory_peak:1022656
used_memory_peak_human:998.69K
used_memory_lua:33792
mem_fragmentation_ratio:0.99
mem_allocator:libc
127.0.0.1:6379> lpush list b     //添加一个节点,value = b
(integer) 2
127.0.0.1:6379> info memory
# Memory
used_memory:1022672              //此时新增节点b后,新增的内存为1022672-1022656 = 16字节
used_memory_human:998.70K
used_memory_rss:991232
used_memory_peak:1022672
used_memory_peak_human:998.70K
used_memory_lua:33792
mem_fragmentation_ratio:0.97
mem_allocator:libc
127.0.0.1:6379> object encoding list
"ziplist"	//表明目前list的编码格式还是压缩列表
---------------------------我是一个可爱的分割线 --------------------------------------------
127.0.0.1:6379> CONFIG set list-max-ziplist-entries 0  //设置不允许使用压缩列表编码
OK
127.0.0.1:6379> del list  //删除key
(integer) 1
------------------------------  继续分割 -------------------------------------------------
127.0.0.1:6379> lpush list a //先通过创建一个list,来初始化字典,主要是减少其它dict等结构内存分配带来的额外计算
(integer) 1
127.0.0.1:6379> info memory
# Memory
used_memory:1039616
used_memory_human:1015.25K
used_memory_rss:950272
used_memory_peak:1039616
used_memory_peak_human:1015.25K
used_memory_lua:33792
mem_fragmentation_ratio:0.91
mem_allocator:libc
127.0.0.1:6379> lpush list1 a
(integer) 1
127.0.0.1:6379> info memory
# Memory
used_memory:1039792			//此处供占用1039792-1039616 = 176字节
used_memory_human:1015.42K
used_memory_rss:950272
used_memory_peak:1039792
used_memory_peak_human:1015.42K
used_memory_lua:33792
mem_fragmentation_ratio:0.91
mem_allocator:libc
127.0.0.1:6379> lpush list1 b //再push一个1字节的元素b
(integer) 2
127.0.0.1:6379> info memory
# Memory
used_memory:1039856   //此处额外占用 1039856-1039792=64字节
used_memory_human:1015.48K
used_memory_rss:901120
used_memory_peak:1039856
used_memory_peak_human:1015.48K
used_memory_lua:33792
mem_fragmentation_ratio:0.87
mem_allocator:libc
127.0.0.1:6379>

从上面的测试中可以看出,同样保存一个字符b,不使用压缩列表,是压缩列表的4倍空间占用。

Why 压缩列表新增一个b占用16字节

新增一个值为b的节点,字节占用多了16byte,按理说应该是size(prevrawlen)+size(len)+size(value)=1+1+1= 3 字节才对。

主要是这样的,一个空的ZipList 占用zlbytes+zltail+zllen+zlend= 4+4+2+1=11字节,默认采用jemalloc 内存分配器,分配16字节
lpush list a
此时ZipList使用了11+3=14字节,尚未触发到扩容
lpush list b
此时zipList 使用了14+3=17字节,触发扩容,内存分配器为其分配到了32 byte,因此整体新增16byte
感兴趣的朋友,可以启用压缩列表,然后push a b 后,在push 一个c,发现内存占用是没有变化的。
Why 新建一个list1占用空间为176字节

因为list1创建需要的内存空间需要这几部分组成

size=sizeof(dictEntry)+sizeof(SDS[key])+sizeof(redisObject[v])+sizeof(list)+sizeof(listNode)+sizeof(redisObject[value])+sizeof(SDS[a])

sizeof(dictEntry) = key+v+next = 8+8+8=24->32;

sizeof(SDS[key])=9+len(list1)+free=9+5+0=14->16;

sizeof(redisObject[v]) = 16;

sizeof(list) 8+8+8+8+8+8=48->48;

sizeof(listNode)=8+8+8->32;

sizeof(redisObject[value])=16;

sizeof(SDS[a])=9+len(a)+free=9+1+0=10->16

所以size= 32+16+16+48+32+16+16=176字节

Why 新添加一个元素b占用64字节

size=sizeof(listNode)+sizeof(redisObject)+sizeof(SDS[b])

sizeof(listNode)=8+8+8->32;

sizeof(redisObject[value])=16;

sizeof(SDS[b])=9+len(b)+free=9+1+0=10->16

size = 32+16+16=64字节

同理大家可以计算下Hash类型的空间占用情况,这里就暂时不表了。

最后:一个渣渣程序员,上面是结合一些资料和源码的主观理解,如果有不对或者疑问的地方,欢迎留言探讨,让我们轻松完爆Redis!期待你的加入。