redis源码阅读之一(zipList)

Redis的ZipList是为节省内存而设计的数据结构,用于存储小型列表和哈希。它包含zlbytes、zltail、zlen和zlend等字段来管理内存和节点。ZipList的每个节点由entry组成,包含prevLength、encoding和content。编码方式决定了content存储的是int还是字节数组及其长度。插入、删除和查找操作在ZipList上执行,插入会涉及级联更新prevLength,删除会移动数据并调整内存。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

zipList是redis为了节约内存开发而设计的,zipList是内存连续的。

zipList的数据结构

下面图即为zipLst的数据结构,其中:

  • zlbytes: 记录整个压缩列表的内存的数量,主要是用来进行对这个压缩列表的内存重新分配

  • zltail: 记录的是这个zipList的起始地址到尾节点的偏移量,通过这个偏移量能直接找到这个链表的尾节点

  • zlen: 主要存储的是整个压缩列表的节点的数量,需要注意的是它是两个字节,当其字节超过65535(包括)时,则需要通过遍历才能找到这个zipList的长度

  • zlend: 这个数据为255,表示的是结束的节点,可以用来判断这个列表是否为空

  • entry: 是存储数据的节点,用于具体存储数据。

    • prevLength: 表示当前节点的前一个节点的长度,通过这个长度即可向前访问节点,需要注意的是它的长度是可变的,当其大小小于254时,即用一个字节表示,当其大于等于254时,即用5个字节表示。
    • encoding 这表示的是后面的content的了类型是int还是字节数组,以及后续节点的长度信息
    • content: 这表示的则是具体的数据内容。

在这里插入图片描述

coding

coding是用来表示content的内容是int还是byte数组,以及content的长度的,首先它通过conding的第一个字节的前两位来确认编码方式。

编码含义编码长度
00content是长度小于0x3f(这个字节的剩余6位就可表示)的字节数组1
01content是长度从(0x3f,0x0x3fff]之间的字节数组2
10content是长度为32位的字节数组5
11content是一个整数不确定

可以看到当前两位的数值位00,01,10时,content为字节数组,当前两位为11时,则表示content为一个整数数值。而这个整数数值到底是多少位的,则需要这个字节后续的编码来确定。

编码content的值
111111108位有符号整数
1100000016位有符号整数
1111000024位有符号整数
1101000032位有符号整数
1110000064位整数
11110000-11111100表示的是0-12的整数值

zipList的具体实现

对于zipList的主要方法是insert,delete和find这三种操作,需要注意的是zipList没有提供update方法,对zipList的节点更新就是将原来的节点删除,然后加入新的节点。

insert方法

插入方法主要有下面几个步骤。

  1. 首先根据传入数据计算出其对应的conding,以及获取其对应的需要插入的节点,然后获取这个节点的preLength,通过prevLength,encoding,content这三个数据即可知道此次插入节点的长度,即为下图中橙色系欸但表示的长度。
    在这里插入图片描述
  2. 然后对当前的zipList进行重新的内存分配,并且将当前节点向后移动这个空间的长度来用于存储新的数据。需要注意的是这个新分配出来的空白内存可能不止新节点的总长度(也有可能比它小),因为新插入的节点的长度可能比当前节点原来的节点的preLength不同,所以有可能这个preLength的编码长度可能会发生改变,因此此处的红色部分即为多分配的用于存储新的当前节点的preLength用的
    在这里插入图片描述
  3. 然后就是更新当前节点的preLength节点为新插入的节点的长度,更新ztail的偏移量为新的节点,并且将新节点数据加入到分配的内存中。
    在这里插入图片描述
  4. 级联更新后续的preLength,这个操作其实是在将节点数据加入到内存之前进行的,不过这个顺序应该影响不大,因为之前改变了当前插入节点的preLength,这个preLength的值的编码大小可能会发生变化,从而引起当前插入节点的后面节点的preLength的变化,从而需要迭代的将后面的节点的preLength。需要注意的是只有preLength的编码变长才会重新分配内存(即从一个字节变成5个字节),当编码变短时,依然用5个字节来表示这个新的数据,而不会对内存进行重新分配
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;
    //表示的是p所指向的节点的前置节点的length的编码的字节长度差
    int nextdiff = 0;
    unsigned char encoding = 0;
    long long value = 123456789; 
    zlentry entry, tail;
    /* Find out prevlen for the entry that is inserted. */
    if (p[0] != ZIP_END) {
        // 如果 p[0] 不指向列表末端,说明列表非空,并且 p 正指向列表的其中一个节点
        // 那么取出 p 所指向节点的信息,并将它保存到 entry 结构中
        // 然后用 prevlen 变量记录前置节点的长度
        // (当插入新节点之后 p 所指向的节点就成了新节点的前置节点)
        // T = O(1)
        entry = zipEntry(p);
        prevlen = entry.prevrawlen;
    } else {
        // 如果 p 指向表尾末端,那么程序需要检查列表是否为:
        // 1)如果 ptail 也指向 ZIP_END ,那么列表为空;
        // 2)如果列表不为空,那么 ptail 将指向列表的最后一个节点。
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
        if (ptail[0] != ZIP_END) {
            // 表尾节点为新节点的前置节点
            // 取出表尾节点的长度
            // T = O(1)
            prevlen = zipRawEntryLength(ptail);
        }
    }
    // 尝试看能否将输入字符串转换为整数,如果成功的话:
    // 1)value 将保存转换后的整数值
    // 2)encoding 则保存适用于 value 的编码方式
    // 无论使用什么编码, reqlen 都保存节点值的长度
    // T = O(N)
    if (zipTryEncoding(s,slen,&value,&encoding)) {
        /* 'encoding' is set to the appropriate integer encoding */
        reqlen = zipIntSize(encoding);
    } else {
        reqlen = slen;
    }
    // 计算编码前置节点的长度所需的大小
    // T = O(1)
    reqlen += zipPrevEncodeLength(NULL,prevlen);
    // 计算编码当前节点值所需的大小
    // T = O(1)
    reqlen += zipEncodeLength(NULL,encoding,slen);
    // 只要新节点不是被添加到列表末端,
    // 那么程序就需要检查看 p 所指向的节点(的 header)能否编码新节点的长度。
    // nextdiff 保存了新旧编码之间的字节大小差,如果这个值大于 0 
    // 那么说明需要对 p 所指向的节点(的 header )进行扩展
    // T = O(1)
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
    // 因为重分配空间可能会改变 zl 的地址
    // 所以在分配之前,需要记录 zl 到 p 的偏移量,然后在分配之后依靠偏移量还原 p 
    offset = p-zl;
    // curlen 是 ziplist 原来的长度
    // reqlen 是整个新节点的长度
    // nextdiff 是新节点的后继节点扩展 header 的长度(要么 0 字节,要么 4 个字节)
    // T = O(N)
    zl = ziplistResize(zl,curlen+reqlen+nextdiff);
    p = zl+offset;
    if (p[0] != ZIP_END) {
        // 新元素之后还有节点,因为新元素的加入,需要对这些原有节点进行调整
        // 移动现有元素,为新元素的插入空间腾出位置
        // T = O(N)
        //这里是将p-nextdiff位置开始向后移动到p-reqlen地位置
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
        // 将新节点的长度编码至后置节点
        // p+reqlen 定位到后置节点
        // reqlen 是新节点的长度
        // T = O(1)
        zipPrevEncodeLength(p+reqlen,reqlen);
        // 更新到达表尾的偏移量,将新节点的长度也算上
        ZIPLIST_TAIL_OFFSET(zl) =
            intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
        // 如果新节点的后面有多于一个节点
        // 那么程序需要将 nextdiff 记录的字节数也计算到表尾偏移量中
        // 这样才能让表尾偏移量正确对齐表尾节点
        // T = O(1)
        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 {
        // 新元素是新的表尾节点
        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
    }
    // 当 nextdiff != 0 时,新节点的后继节点的(header 部分)长度已经被改变,
    // 所以需要级联地更新后续的节点
    if (nextdiff != 0) {
        offset = p-zl;
        // T  = O(N^2)
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }
    // 一切搞定,将前置节点的长度写入新节点的 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;
}

delete方法

下面图展示了这个zipList的删除方法的主要的操作,下面的灰色部分为要删除的节点,首先更新需要删除节点的后续节点的preLength以及ztail的数据,更新完以后将要删除节点的节点的内存向前移动,然后进行内存重新设置,清除多余的内存,当然最后还是会级联更新后续节点的preLength数值。
在这里插入图片描述

static unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
    unsigned int i, totlen, deleted = 0;
    size_t offset;
    int nextdiff = 0;
    zlentry first, tail;
    // 计算被删除节点总共占用的内存字节数
    // 以及被删除节点的总个数
    // T = O(N)
    first = zipEntry(p);
    for (i = 0; p[0] != ZIP_END && i < num; i++) {
        p += zipRawEntryLength(p);
        deleted++;
    }
    // totlen 是所有被删除节点总共占用的内存字节数
    totlen = p-first.p;
    if (totlen > 0) {
        if (p[0] != ZIP_END) {
            // 执行这里,表示被删除节点之后仍然有节点存在
            // 因为位于被删除范围之后的第一个节点的 header 部分的大小
            // 可能容纳不了新的前置节点,所以需要计算新旧前置节点之间的字节数差
            // T = O(1)
            nextdiff = zipPrevLenByteDiff(p,first.prevrawlen);
            // 如果有需要的话,将指针 p 后退 nextdiff 字节,为新 header 空出空间
            p -= nextdiff;
            // 将 first 的前置节点的长度编码至 p 中
            // T = O(1)
            zipPrevEncodeLength(p,first.prevrawlen);
            /* Update offset for tail */
            // 更新到达表尾的偏移量
            // T = O(1)
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen);
            // 如果被删除节点之后,有多于一个节点
            // 那么程序需要将 nextdiff 记录的字节数也计算到表尾偏移量中
            // 这样才能让表尾偏移量正确对齐表尾节点
            // T = O(1)
            tail = zipEntry(p);
            if (p[tail.headersize+tail.len] != ZIP_END) {
                ZIPLIST_TAIL_OFFSET(zl) =
                   intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
            }
            // 从表尾向表头移动数据,覆盖被删除节点的数据
            // T = O(N)
            memmove(first.p,p,
                intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
        } else {
            // 执行这里,表示被删除节点之后已经没有其他节点了
            // T = O(1)
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe((first.p-zl)-first.prevrawlen);
        }
        // 缩小并更新 ziplist 的长度
        offset = first.p-zl;
        zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
        ZIPLIST_INCR_LENGTH(zl,-deleted);
        p = zl+offset;
        // 如果 p 所指向的节点的大小已经变更,那么进行级联更新
        // 检查 p 之后的所有节点是否符合 ziplist 的编码要求
        // T = O(N^2)
        if (nextdiff != 0)
            zl = __ziplistCascadeUpdate(zl,p);
    }

    return zl;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值