数据结构 -- 压缩列表(Ziplist)

压缩列表

ziplist是一个经过特殊编码的双向链表,旨在提高内存效率。 它存储字符串和整数值,其中整数被编码为实际整数而不是一系列字符。ziplist在任意一侧的PushPop操作时间复杂度都是 O ( 1 ) O(1) O(1)。但是每个操作都需要重新分配ziplist的内存,因此实际复杂性与ziplist使用的内存量有关

创建

与前面几个数据结构不一样。Redis并没有直接给出ziplist的定义。但是我们可以从ziplist的创建函数ziplistNew中推出ziplist的结构

#define ZIP_END 255 
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))
#define ZIPLIST_END_SIZE        (sizeof(uint8_t))
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))

unsigned char *ziplistNew(void) {
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;      // 头部大小+尾部大小 10bytes
    unsigned char *zl = zmalloc(bytes);                             // 开辟空间
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);                        // 将[0,4]转为uint32_t类型 并且将(头部+尾部)所占字节写入
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);    // 将[4,8]转为uint32_t类型 并且将头部大小写入
    ZIPLIST_LENGTH(zl) = 0;                                         // 将[8,10]转为uint16_t类型 并且将长度写入
    zl[bytes-1] = ZIP_END;                                          // 将ZIP_END写入尾部
    return zl;
}

一个空的ziplist的内存结构大致如下图

在这里插入图片描述

添加一个元素

一个空的ziplist并不能确定其真的数据元素在内存中是如何布局的,解下来直接看添加元素的源码

// zlentry并不是数据实际的编码方式,而是用来解析ziplist中的数据的。这样更加方便对数据的操作
typedef struct zlentry {
    unsigned int prevrawlensize;        // 前一个元素编码长度所占字节数
    unsigned int prevrawlen;            // 前一个元素编码长度
    unsigned int lensize;               // 用于编码此节点类型/长度的字节
    unsigned int len;                   // 用于表示节点实际的字节
    unsigned int headersize;            // prevrawlensize + lensize 用于表示节点头部的字节
    unsigned char encoding;             // 节点的编码类型 设置为ZIP_STR_*或ZIP_INT_*,具体取决于节点编码
    unsigned char *p;                   // 第一个节点的地址指针
} zlentry;

unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    // 在p处插入一个元素,元素的值为s,长度为slen
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen, newlen;
    unsigned int prevlensize, prevlen = 0;
    size_t offset;
    int nextdiff = 0;
    unsigned char encoding = 0;
    long long value = 123456789; // initialized to avoid warning
    zlentry tail;
    if (p[0] != ZIP_END) {      // 说明p指向的是ziplist中间的某个元素的结束位置
        // 获取p指向的元素的长度(这里涉及到一个变长整数的解码,有兴趣可以看一下宏的具体内容)
        ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
    } else {                    // p[0] == ZIP_END  说明p指向的是ziplist的最后一个字节
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);      // ptail指向最后一个元素,如果ziplist是空,则指向尾部
        if (ptail[0] != ZIP_END) {
            // 说明ziplist不是空,求最后一个元素的长度。因为后续新元素要存储前一个元素的长度
            prevlen = zipRawEntryLengthSafe(zl, curlen, ptail);
        }
    }
    // reqlen为插入元素在ziplist占的总内存大小
    //  1.数据元素的长度
    if (zipTryEncoding(s,slen,&value,&encoding)) {
        reqlen = zipIntSize(encoding);
    } else {
        reqlen = slen;
    }
    //  2.存储前一个元素长度需要的空间
    reqlen += zipStorePrevEntryLength(NULL,prevlen);
    //  3.存插入元素编码需要的空间
    reqlen += zipStoreEntryEncoding(NULL,encoding,slen);
    
    // 如果插入的新元素不是在ziplist的尾部,那么要确保新元素后的一个元素能存下新元素的长度,否则需要对后续元素扩容
    int forcelarge = 0;
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
    if (nextdiff == -4 && reqlen < 4) {
        nextdiff = 0;
        forcelarge = 1;
    }

    offset = p-zl;                      // ziplist起始地址到插入点的偏移量
    newlen = curlen+reqlen+nextdiff;    // 新的ziplist所需要的内存字节数
    zl = ziplistResize(zl,newlen);      // 重新分配内存,并且最后一个字节以及设置为ZIP_END
    p = zl+offset;                      // p指向即将插入元素的起始位置
    if (p[0] != ZIP_END) {
        // 如果插入的元素不是在ziplist的尾部,那么需要将后续元素往后移动
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
        if (forcelarge)
            zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
        else
            zipStorePrevEntryLength(p+reqlen,reqlen);
        // 更新ziplist的偏移量
        ZIPLIST_TAIL_OFFSET(zl) =
            intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
        assert(zipEntrySafe(zl, newlen, p+reqlen, &tail, 1));
        if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
            // 新加入的元素后面还有元素,需要更新偏移量
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
        }
    } else {
        // 在ziplist的尾部插入元素,插入的元素就是尾部元素,直接更新ziplist头部保存的偏移量
        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
    }

    if (nextdiff != 0) {
        // 说明插入元素后面的元素不能存储插入元素的长度,需要扩容  --- 注意这里可能会引起连锁更新
        offset = p-zl;
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }

    // 保存值
    p += zipStorePrevEntryLength(p,prevlen);
    p += zipStoreEntryEncoding(p,encoding,slen);
    if (ZIP_IS_STR(encoding)) {
        memcpy(p,s,slen);
    } else {
        zipSaveInteger(p,value,encoding);
    }
    ZIPLIST_INCR_LENGTH(zl,1);
    return zl;
}

从插入一个元素的源码中,我们可以看出一个非空的ziplist的内存结构如下

在这里插入图片描述

其他操作

其实在理解了ziplist的内存结构之后,其他操作就很好理解了,这里就不再赘述了(主要是懒)。需要注意的是在删除元素和添加元素时ziplist都有可能引发连锁更新

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虎小黑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值