redis源码学习--数据结构:ziplist实现

本文接上篇"redis源码学习–数据结构:ziplist设计"
https://blog.csdn.net/dmgy614262711/article/details/105879969

一下是entry定义的数据结构

/* We use this function to receive information about a ziplist entry.
 * Note that this is not how the data is actually encoded, is just what we
 * get filled by a function in order to operate more easily. */
typedef struct zlentry {
    unsigned int prevrawlensize; /* 记录前一个entry的长度所占的字节数 Bytes used to encode the previous entry len */
    unsigned int prevrawlen;     /* 前一个entry的字节数  Previous entry len. */
    unsigned int lensize;        /* 本entry的用于保存encoding和数据长度的字节数 Bytes used to encode this entry type/len.
                                    For example strings have a 1, 2 or 5 bytes
                                    header. Integers always use a single byte.*/
    unsigned int len;            /*  表示本entry数据的长度 Bytes used to represent the actual entry.
                                    For strings this is just the string length
                                    while for integers it is 1, 2, 3, 4, 8 or
                                    0 (for 4 bit immediate) depending on the
                                    number range. */
    unsigned int headersize;     /* 头长度 prevrawlensize + lensize. */
    unsigned char encoding;      /* 编码方式 Set to ZIP_STR_* or ZIP_INT_* depending on
                                    the entry encoding. However for 4 bits
                                    immediate integers this can assume a range
                                    of values and must be range-checked. */
    unsigned char *p;            /* 指向entry的指针 Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;

我们学习完结构体定义之后可以直接先看测试用例代码,顺着测试用例的流程来学习ziplist的实现。
测试用例总入口为ziplistTest
先看第一个源码函数ziplistNew,新建一个ZIPList,新建即对zlentry结构赋初始值

/* The size of a ziplist header: two 32 bit integers for the total
 * bytes count and last item offset. One 16 bit integer for the number
 * of items field. */
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))

/* Size of the "end of ziplist" entry. Just one byte. */
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))

/* Create a new empty ziplist. */
unsigned char *ziplistNew(void) {
	// // ziplist固有字段的长度,不包含entry, 分别是zlbytes+zltail + zllen + zlend
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE; 
    unsigned char *zl = zmalloc(bytes);
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes); // 把bytes转化成小端序,赋值给zlbytes
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE); //zltail赋值 
    ZIPLIST_LENGTH(zl) = 0; // zllen赋值
    zl[bytes-1] = ZIP_END; // zlend赋值
    return zl;
}

接下来看插入函数ziplistPush,先找到p指针,如果是从表头插入,p指向zllen后面(第一个entry)的位置;如果是表尾插入,指向zlend字段。由于zlend字段固定位255,所以可以根据p[0]是否为255来识别是查头还是插尾。(这也应该是entry的第一个字节最大只能为254的原因)。
知识点:p指针的赋值使用了宏来封装。避免直接对指针地址进行操作,更加安全。也方便适用于其他地方的使用。

/* 
zl:表头
s:待插入的数据
slen:数据长度
where:插头还是差尾
 */
unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {
    unsigned char *p;
    p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
    return __ziplistInsert(zl,p,s,slen);
}

封装了一个内部函数__ziplistInsert(zl,p,s,slen),函数过长,就不贴了。如果从头插入,P的值是从list地址头偏移2个U32+1个U16的地址,从标位插入,那么p的值是表头+zllen-1,即zlend的地址,这样,如果p中第一个字节是255,表示是尾差,否则是头插。
__ziplistInsert函数的总结起来就是:
1、组装新的entyry结构

字段 解释
prevlen 表示前一个entry占用的字节数,占1个字节或者5个字节,第一个字节为0xFE表示需要5个字节表示
encoding 表示编码格式,包含了编码格式和编码后数据的长度
entry-data 表示编码后的值

对于从头插来说,prevlen值就是0,占用是1个字节。
软件中计算entry字段的长度代码如下,先检查是否可以被encode,可以则计算encoding字段所需的长度,在计算prelen字段的长度,最后计算entry-data的长度。
redis源码学习--数据结构:ziplist实现_第1张图片
如果是头插,需要刷新下一个entry的prelen字段,如果原先prelen字段的长度不够,那就需要扩充到5个字节,意味的之后的所有的结构都要调整位置,毕竟zipList是内存连续存储的。下面代码检查下一个entry的prelen字段的长度是否足够,如果不够,nextdiff就是额外需要的长度。源码中只检查了下一个entry是否需要扩充长度,实际上如果下一个字段长度扩充了,那么再下一个entry的prelen也要刷新,一直到最后一个。源码中在后面有专门的处理。
redis源码学习--数据结构:ziplist实现_第2张图片
计算出下一个entry的prelen字段是否需要扩充,扩充保留在nextdiff(取值为0,4或-4)中。负数表示从5份字节变为4个字节,forcelarge值会置1。
之后是重新申请内存,代码如下。由于内存块可能会会更换,所以需要先算出offset,才能恢复p的位置。
在这里插入图片描述
如果是尾插,到此可以直接刷新zltail字段。代码如下:
redis源码学习--数据结构:ziplist实现_第3张图片
头插比较复杂,代码如下分为4步:
1)移动内存,留出供新entry的位置。
2)根据之前的计算结果调整下一个entry的prevlen和prevlen所占的长度
3)刷新zltail字段(表示ziplist头指针到最后一个entry的偏移地址),先不包含nextdiff。如果下一个entry是最后一个entry,自己长度的变化不会影响zltail字段。否则zltail需要需要加入nextdiff,下一步即此处理。
4)获取出下一个entry的结构,判断是否是最后一个entry,如果不是,那需要刷新zltail字段,加上nextdiff
redis源码学习--数据结构:ziplist实现_第4张图片
如果nextdiff的值不为0,意味着下一个entry的长度需要调整,同理需要逐个后面的entry,看是否刷新prevlen以及其所占的内存。代码如下:
redis源码学习--数据结构:ziplist实现_第5张图片
调整完所有原先的entry之后封装新的entry,代码如下,不做解释
redis源码学习--数据结构:ziplist实现_第6张图片
至此,插入一个entry的流程完毕。

总结:
1、代码中对于结构体元素的取值和赋值都使用了宏封装,屏蔽细节,使代码更加紧凑,比如ZIP_DECODE_PREVLEN,ZIPLIST_ENTRY_TAIL。
2、代码注释丰富,没个功能块都有注释。
3、主函数__ziplistInsert分层清晰,都是对ziplist级别的元素进行处理,其他低层的细节都封装后直接调用
4、相对于尾差,前插效率更低,因为可能需要调整后面没个entry的prevlen字段,并且可能还需要扩充后面entry的内存空间。

下一步我们计划学习函数__ziplistCascadeUpdate。

你可能感兴趣的:(数据结构(C语言):链表,c语言)