Redis源码阅读笔记(五)ziplist压缩列表结构

- ziplist压缩列表简介

ziplist同intset一样是Redis独有的,主要是为了节约内存,提高存储效率而产生出来的,经过了特殊编码的双向链表。但是与双向链表不同的是,ziplist是一块连续的内存,在这块连续的内存中不同的节点可以是字符串也可以是整数。同时对整数的存储也是使用了变长编码的方式,以此来更进一步的节约内存。存储模式是小端模式。

没有用自定义的struct之类的来表达,而就是简单的unsigned char *。这是因为ziplist本质上就是一块连续内存,内部组成结构又是一个高度动态的设计(变长编码),也没法用一个固定的数据结构来表达。

因为ziplist里面每个数据项开头都有前置节点长度和当前节点长度,所以可以像双端链表那样进行遍历。

- ziplist压缩列表结构

虽然说ziplist被当做了数据类型的一种,但是由于它是一块连续的内存,所以可以和sds一样只需要一个char*指针即可表示。而它内部的每一个节点都有自己的构成。
ziplist内存结构如下:

< zlbytes > < zltail > < zllen > < entry 1>< entry2 >… < entryN > < zlend >

< zlbytes >:32bit,表示ziplist所占字节数,是一个无符号数(也包括< zlbytes >本身所占4字节);
< zltail >:32bit,表示ziplist表中最后一项entry在ziplist中的偏移字节数;
< zllen >:16bit,表示ziplist表中数据项的个数,当可表达的个数超过2^16-1时,ziplist仍然可以使用,只是zllen不再有效,此时若想要知道长度就只能通过遍历。
< entry >:真正存放数据的数据项,每一个节点长度视内容而定,每个节点又有自己的格式 :< prevrawlen >< len > < data >
< zlend >:一个字节 1111 1111 代表结束标识符 所以通常在ziplist中表示长度的标志位都不会使用255这个数值。

- < entry >数据节点的构成

该结构体在实际ziplist存储中是并不存在的,在实际中每个ziplist中的每个entry的格式都依旧是一段连续的内存保存着< prevrawlen >< len > < data >这样一段数据作为一个节点的。该结构体只是在部分函数API中内部根据内存数据生成的以方便进行各种修改操作。

typedef struct zlentry {
    //前置节点长度,和编码该长度用的字节数,可能为1个字节或者5个字节,取决于前面是不是小于254个字节
    unsigned int prevrawlensize, prevrawlen;   
    //当前节点长度和编码该长度用的字节数
    unsigned int lensize, len;                   
    //prevrawlensize+lensize 当前节点总字节数
    unsigned int headersize;    
    //本节点编码方式 9种 字符三种00 01 10  数字6种2/4/8/3/1字节和4bit表示的0-12 其中若表示0-12可以直接存储不需要指针 
    unsigned char encoding;
    //指向当前节点的指针,这个指针是开始位置       
    unsigned char *p;           
} zlentry;
  • 前置节点长度< prerawlen >:
    这里的prelen可已由一个字节表示也可以由5个字节表示。
    - 当前一节点长度小于254时就只需要一个字节8个bit就足够表示,其中不使用255是因为255被当做了结束标志。当一个节点开头是255那么说明该ziplist结束了。
    - 当前一节点长度大于等于254时就会使用两个字节来存储prerawlen,第一个字节为254(1111 1110)作为标志,后面4个字节组成一个整型用来存储长度。

  • 当前节点长度< len >:
    本字段非常的复杂,本字段既用来标志存储数据的类型也用来表示数据项的长度。总共分为9种情况,其中字符串占三种,数字占6种。

    字段开头为00/01/10代表后面数据项为字符串:
    00xxxxxx 表明用6个bit位来表示长度,说明数据项长度在[0,2^6-1]之间
    01xxxxxx xxxxxxxx 表明用14个bit位来表示长度,说明数据项长度在[26,214-1]之间
    11_____ xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 表明用32bit来表示长度,第一个字节后六位置空不用,说明数据项长度在[214,232-1]之间

字段开头为11代表后面数据项为数字:
1100 0000 --------------- int16_t类型整数
1101 0000 --------------- int32_t类型整数
1110 0000 ----------------- int64_t类型整数
1111 0000 ----------------- 24bit有符号整数
1111 1110 ------------------ 8bit有符号整数
1111 xxxx ------------------ 4bit无符号整数,表示[0,12] 若是该种情况,len编码直接存储了数据不需要额外的数据项。

- ziplist插入逻辑 ``` /* Insert item at "p". * * 根据指针 p 所指定的位置,将长度为 slen 的字符串 s 插入到 zl 中。 插入在p前面,p统一后移 * * 函数的返回值为完成插入操作之后的 ziplist * * T = O(N^2) */ static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) { size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen; unsigned int prevlensize, 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 tail;
/* Find out prevlen for the entry that is inserted. */
if (p[0] != ZIP_END) {
    ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);    //取出p处的前置节点长度值和编码该长度值的大小
} else {
    // 如果 p 指向表尾末端,那么程序需要检查列表是否为:
    // 1)如果 ptail 也指向 ZIP_END ,那么列表为空;
    // 2)如果列表不为空,那么 ptail 将指向列表的最后一个节点。
    unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
    if (ptail[0] != ZIP_END) {
        prevlen = zipRawEntryLength(ptail);    //取出表尾节点的长度   进入这里表明表尾节点将成为新节点的前置节点
    }
}

/* See if the entry can be encoded */
// 尝试看能否将输入字符串转换为整数,如果成功的话:
// 1)value 将保存转换后的整数值
// 2)encoding 则保存适用于 value 的编码方式
// 无论使用什么编码, reqlen 都保存节点值的长度
if (zipTryEncoding(s,slen,&value,&encoding)) {
    /* 'encoding' is set to the appropriate integer encoding */
    reqlen = zipIntSize(encoding);
} else {
    /* 'encoding' is untouched, however zipEncodeLength will use the
     * string length to figure out how to encode it. */
    reqlen = slen;
}
/* We need space for both the length of the previous entry and
 * the length of the payload. */
reqlen += zipPrevEncodeLength(NULL,prevlen);
reqlen += zipEncodeLength(NULL,encoding,slen);

/* 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. */
// 只要新节点不是被添加到列表末端,
// 那么程序就需要检查看 p 所指向的节点(的 header)能否编码新节点的长度。
// nextdiff 保存了新旧编码之间的字节大小差,如果这个值大于 0 
// 那么说明需要对 p 所指向的节点(的 header )进行扩展
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;

/* Store offset because a realloc may change the address of zl. */
offset = p-zl;
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 */
    memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);

    /* Encode this entry's raw length in the next entry. */
    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. */
    tail = zipEntry(p+reqlen);
    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 */
if (nextdiff != 0) {
    offset = p-zl;
    zl = __ziplistCascadeUpdate(zl,p+reqlen);
    p = zl+offset;
}

/* Write the entry */
p += zipPrevEncodeLength(p,prevlen);
p += zipEncodeLength(p,encoding,slen);
if (ZIP_IS_STR(encoding)) {
    memcpy(p,s,slen);
} else {
    zipSaveInteger(p,value,encoding);
}
// 更新列表的节点数量计数器
ZIPLIST_INCR_LENGTH(zl,1);
return zl;

}

插入函数解析:

- 这个函数是在指定的位置p插入一段新的数据,待插入数据的地址指针是s,长度为slen。插入后形成一个新的数据项,占据原来p的配置,原来位于p位置的数据项以及后面的所有数据项,需要统一向后移动,给新插入的数据项留出空间。参数p指向的是ziplist中某一个数据项的起始位置,或者在向尾端插入的时候,它指向ziplist的结束标记< zlend >。
- 函数开始先计算出待插入位置前一个数据项的长度prevlen。这个长度要存入新插入的数据项的< prevrawlen >字段。
- 然后计算当前数据项占用的总字节数reqlen,它包含三部分:< prevrawlen > , < len >和真正的数据。其中的数据部分会通过调用zipTryEncoding先来尝试转成整数。
- 由于插入导致的ziplist对于内存的新增需求,除了待插入数据项占用的reqlen之外,还要考虑原来p位置的数据项(现在要排在待插入数据项之后)的< prevrawlen >字段的变化。本来它保存的是前一项的总长度,现在变成了保存当前插入的数据项的总长度。这样它的< prevrawlen >字段本身需要的存储空间也可能发生变化,这个变化可能是变大也可能是变小。这个变化了多少的值nextdiff,是调用zipPrevLenByteDiff计算出来的。如果变大了,nextdiff是正值,否则是负值。
- 现在很容易算出来插入后新的ziplist需要多少字节了,然后调用ziplistResize来重新调整大小。ziplistResize的实现里会调用allocator的zrealloc,它有可能会造成数据拷贝。
- 现在额外的空间有了,接下来就是将原来p位置的数据项以及后面的所有数据都向后挪动,并为它设置新的< prevrawlen >字段。此外,还可能需要调整ziplist的< zltail >字段。
- 最后,组装新的待插入数据项,放在位置p。

你可能感兴趣的:(Redis源码阅读笔记(五)ziplist压缩列表结构)