【redis源码阅读】ziplist


前言

基于带中文注释的redis3.0源码阅读,github地址:https://github.com/huangz1990/redis-3.0-annotated。redis的基础数据结构ziplist的源码位于ziplist.c。


一、数据结构

ziplist实际上没有具体的数据结构,而是一块连续的内存。redis提供对这块内存操作的api,从而达到数据增删改查的功能。
为了达到这个目的,我们需要对这块内存做出一些规定,不能让数据随便放在里面,否则就找不到了。一个空ziplist在内存中按下表所示组织

  • zlbytes表示这个ziplist占用了多少字节,zlbytes大小为4字节,这里zlbytes是11(二进制1011)
  • zltail表示ziplist的尾部的偏移量,zltail大小为4字节,这里zltail值是10(二进制1010)
  • zllen表示ziplist元素个数,大小占2字节
  • zlend表示ziplist尾部,大小1字节,值是固定的1111 1111
zlbyteszltailzllenzlend
1011101001111 1111
大小4 bytes4 bytes2 bytes1 bytes

在zllen后面存放的是ziplist的元素entry。

zlbyteszltailzllenentryentryentryzlend
4 bytes4 bytes2 bytes???1 bytes

其中每个entry内按顺序分为三部分

  • 第一部分编码了前一个entry相关信息,可以通过它找到前一个节点,相当于链表的prev指针
  • 第二部分编码当前节点数据的编码和长度信息,可以通过它找到下一个节点,相当于next指针,也可以用它找到节点存储的数据
  • 第三部分是数据本身

redis将前一个entry的信息按自定义编码规则存放到entry的第一部分内存,将当前数据信息放到第二部分内存。现在看起来还比较抽象,下面通过实际案例来讲解。

二、编码规则

每个entry中涉及编码的就是前一个entry的信息和当前数据信息

1.前一个entry信息编码

每个entry内的第一个部分的内存主要存放前一个entry占多大字节,这里分两种情况
1、前一个entry所占字节小于254,这种情况第一部分的内存大小只占1个字节,该字节中的数据即为前一个entry所占字节大小
2、前一个entry所占字节大于等于254,1个字节存不下了,这种情况第一部分的内存大小占5个字节,第一个字节固定为254(二进制11111110),后面4个字节为前一个entry所占字节大小

可以看到,为了尽量节省内存,redis分情况来编码前一个entry的信息,使用ziplist时应该尽量存储较小的元素,否则这个5个字节的编码信息都赶上一个指针了。

因此获取前一个entry长度信息的算法如下
1、获取当前entry内第一个字节,看看是否小于254
2、小于254则直接返回第一个字节的数据
3、否则返回后面4个字节的数据
代码如下

// ZIP_BIGLEN = 254,该函数获取entry内第一个字节,判断是否小于254
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do {                          \
    // ptr[0]是entry内第一个字节的数据,ptr指向entry
    // prvlnsiz表示编码前一个entry长度所需字节大小
    if ((ptr)[0] < ZIP_BIGLEN) {                                               \
        (prevlensize) = 1;                                                     \
    } else {                                                                   \
        (prevlensize) = 5;                                                     \
    }                                                                          \
} while(0);

#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do {                     \
// 该函数计算前一个entry的长度,保存到prevlen中
// prevlensize保存编码前一个entry信息所需字节大小
    /* 先计算被编码长度值的字节数 */                                           \
    ZIP_DECODE_PREVLENSIZE(ptr, prevlensize);                                  \
                                                                               \
    /* 再根据编码字节数来取出长度值 */                                         \
    if ((prevlensize) == 1) {                                                  \
    	//直接保存第一个字节数据
        (prevlen) = (ptr)[0];                                                  \
    } else if ((prevlensize) == 5) {                                           \
        assert(sizeof((prevlensize)) == 4);                                    \
        //保存后面4个字节的数据
        memcpy(&(prevlen), ((char*)(ptr)) + 1, 4);                             \
        memrev32ifbe(&prevlen);                                                \
    }                                                                          \
} while(0);

2.当前数据信息

根据entry内第一个字节的数据,如果小于254,那么下一个字节就是我们说的第二部分内存,否则4个字节后的内存才是。

第二部分的内存按以下编码规则读取:
先看前两位:
1、前两位为00,那么entry存储的数据是长度小于等于63字节的字符串,且第二部分内存仅占1字节,如 |00pppppp|,后6位的pppppp就是data的字节长度
2、前两位为01,那么entry存储的数据是长度小于等于16383字节的字符串,第二部分内存占2字节,如 |01pppppp|qqqqqqqq|,后14位是data的字节长度
3、前两位为10,那么entry存储的数据是长度大于等于16384字节的字符串,第二部分内存占5字节,如 |10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt|,使用后面4个字节存储data的字节长度
4、前两位为11,那么entry存储的数据是整数,且第二部分内存仅占1字节
4.1、该字节数据为|11000000|表示data为int16_t 类型的整数,长度为 2 字节
4.2、该字节数据为|11010000|表示data为int32_t 类型的整数,长度为 4 字节
4.3、该字节数据为|11100000|表示data为int64_t 类型的整数,长度为 8 字节
4.4、该字节数据为|11110000|表示data为24 位(3 字节)长的整数
4.5、该字节数据为|11111110|表示data为8 位(1 字节)长的整数
4.6、除了上面几种情况,还有|1111xxxx|,因为11110001-11111101的连续数据还没被用到,因此redis用这几种情况表示data为0-12的整数(这样就没有后面的data内存了),以此进一步减少空间。

其实这个字节内还有很多情况没用到,如果多写几个if else是不是可以存比12更大一点的数据[doge]。或者改下编码方式11000000-11000101表示上面5种情况,11000110-11111111用来存data,不清楚redis使用这种编码方式的原因。。。

3.编码案例

到这里我们了解了ziplist在内存中的分布情况以及它的编码规则。下面来看个案例:我们在ziplist中插入字符串’hello world\0’和整数0.

  • 第一个entry的prevlenencode是0表示该entry是ziplist第一个entry。
  • dataencode为00001100,前两位00表示data的长度所需字节小于等于63,后6位001100表示data的长度为12.
  • 我们知道,一个字符占1个字节,所以在data的12个字节中,存放的是’hello world\0’的每一个字符的ascii编码,注意字符串要以’\0’结尾
  • 第二个entry的prevlenencode为14,占1个字节,表示前一个entry的整个长度占14字节,这样我们拿到entry2的指针就可以通过14这个偏移量找到前一个entry
  • 第二个entry的dataencode为11110001,前两位11表示data是一个整数,而且属于上面的4.6情况,data的数据直接保存在dataencode中,data的值为0.
zllen1_prevlenencode1_dataencodedata2_prevlnencode2_dataencodedatazltail
000001100‘hello world\0’0000111011110001
1 byte1 byte12 bytes1 byte1 byte0 byte

三、增删改查

了解了ziplist的内存结构和编码规则,我们应该对如何增删改查有一定的思路了

  • 查找的过程:从ziplist头开始,解码出每个entry的data进行对比,通过prevlenencode和dataencode解码出正确的偏移量移动指针找到下一个entry,直到找到为止。
  • 添加元素的过程:1.查找要插入的位置;2.计算插入元素的prevlenencode+dataencode+data所需字节数;3.扩大ziplist内存区域大小,把当前位置的内存向后移动为插入entry腾出空间;4.把插入entry的信息写入内存;5.因为插入entry可能导致后面节点的prevlenencode发生变化,所以要调整后面节点的prevlenencode,可能会有扩容和缩容(prevlenencode可能从1字节变5字节或5字节变1字节)
  • 删除元素的过程:1.查找要删除的元素;2.找到删除元素的下一个元素,把内存向前移动;3.调整后面entry的prevlenencode
  • 修改元素和删除差不多:也是找到元素,然后修改,最后调整后面entry的prevlenencode

插入

下面提供插入元素的代码加深理解。其他操作的就不贴了,理解了插入后别的应该都能理解。

/*
 * 根据指针 p 所指定的位置,将长度为 slen 的字符串 s 插入到 zl 中。
 * 函数的返回值为完成插入操作之后的 ziplist
 * T = O(N^2)
 */
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    // curlen记录当前 ziplist 的长度,reqlen是插入节点所需字节长度,prevlen是前一个节点长度
    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的结构体
    // ziplist不会存储这样的结构体,而是在使用时解码出来
    zlentry entry, tail;

	
	// 找prevlen
    if (p[0] != ZIP_END) {
        // 如果 p[0] 不指向列表末端,说明列表非空,并且 p 正指向列表的其中一个节点
        // 那么取出 p 所指向节点的信息,并将它保存到 entry 结构中
        // 然后用 prevlen 变量记录前置节点的长度
        // (当插入新节点之后 p 所指向的节点就成了新节点的前置节点)
        // T = O(1)
        entry = zipEntry(p); // 解码zlentry,按照我们上面的编码规则来解码
        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)) {
        reqlen = zipIntSize(encoding); // data所需大小
    } else {
        reqlen = slen; // data所需大小
    }
    // 计算prevlenencode所需的大小
    // T = O(1)
    reqlen += zipPrevEncodeLength(NULL,prevlen);
    // 计算dataencode所需的大小
    // T = O(1)
    reqlen += zipEncodeLength(NULL,encoding,slen);
    // 到这里计算完了reqlen,即插入节点整个的大小

    // 只要新节点不是被添加到列表末端,
    // 那么程序就需要检查看 p 所指向的节点(的 prevlenencode)能否编码新节点的长度。
    // nextdiff 保存了新旧编码之间的字节大小差,如果这个值大于 0 
    // 那么说明需要对 p 所指向的节点以及后续节点进行扩展
    // 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;
    // 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)
        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 {
        /* This element will be the new tail. */
        // 新元素是新的表尾节点
        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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值