redis源码学习之压缩列表

压缩列表

列表键和哈希键的底层实现。是为了节约内存而实现。

压缩列表是一段连续的内存,每个属性都会有固定的编码大小,例如对于字符串来说,我们需要知道字符串的长度,假设小于63字节,那么我们只需要一个字节的大小来表示(2位标识,6位数据);而存储的结构是整型的数的话,我们只需要1个字节来表示该整型是16/32/64位整型。

压缩列表用一段连续内存表示unsigned char *类型指针来访问,不过它人为的规定了这一段连续内存的数据类型:
前4个字节(uint32_t)表示整个ziplist的长度所占用的内存数:zlbytes;
再往后4个字节表示表尾节点到压缩列表的字节数:zltail;
然后2个字节(uint16_t)表示压缩列表中节点数目:zllen;
之后就是数据内容zlentry;
最后压缩列表有1个字节的特殊值标记列表末尾:zlend:0xFF

根据上述说明,redis定义了一些宏来获取各个元素的值,主要是进行地址运算,因为header的大小固定:

// 定位到 ziplist 的 bytes 属性,该属性记录了整个 ziplist 所占用的内存字节数
// 用于取出 bytes 属性的现有值,或者为 bytes 属性赋予新值
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))
// 定位到 ziplist 的 offset 属性,该属性记录了到达表尾节点的偏移量
// 用于取出 offset 属性的现有值,或者为 offset 属性赋予新值
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
// 定位到 ziplist 的 length 属性,该属性记录了 ziplist 包含的节点数量
// 用于取出 length 属性的现有值,或者为 length 属性赋予新值
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
// 返回 ziplist 表头的大小
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))
// 返回指向 ziplist 第一个节点(的起始位置)的指针
#define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)
// 返回指向 ziplist 最后一个节点(的起始位置)的指针
#define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
// 返回指向 ziplist 末端 ZIP_END (的起始位置)的指针
#define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)

对字节长度进行编码,有两个数据类型,一个是字节长度,一个是编码字节长度所需要的长度。

在Redis设计与实现中,给了两个表格,分别是字节数组编码(在源码中又叫字符串),整数编码。

现在给定一个长度len,需要对其进行编码。首先源码中定义了一些宏,分别表示不同字节范围表示的编码。

/*
 * 字符串编码类型
 */
#define ZIP_STR_06B (0 << 6)//长度小于等于63字节
#define ZIP_STR_14B (1 << 6)//长度小于等于16383字节
#define ZIP_STR_32B (2 << 6)//长度小于等于4294967295字节

/*
 * 整数编码类型
 */
#define ZIP_INT_16B (0xc0 | 0<<4)//1100 0000
#define ZIP_INT_32B (0xc0 | 1<<4)//1101 0000
#define ZIP_INT_64B (0xc0 | 2<<4)//1110 0000
#define ZIP_INT_24B (0xc0 | 3<<4)//1111 0000
#define ZIP_INT_8B 0xfe//1111 1110

这种编码方式很好理解,对于长度小于等于63字节编码,编码方式应该是00xxxxxx,除了高位两位的标识位,后六位能表示的数字范围为0~63,这是1个字节的情况。当数据变大时,一个字节显然不能满足条件了,因此就需要加一个字节(8+6 位)的编码长度来表示长度。

如果是整型类型编码就容易多了,不同于字节数组需要记录数据长度,整型数据总是只需要1个字节来表示整型数据类型。

在redis源码src/ziplist.c中用zipEncodeLength函数来描述上述过程。

static unsigned int zipEncodeLength(unsigned char *p, unsigned char encoding, unsigned int rawlen) {
    unsigned char len = 1, buf[5];

    // 编码字符串
    if (ZIP_IS_STR(encoding)) {
        /* Although encoding is given it may not be set for strings,
         * so we determine it here using the raw length. */
        if (rawlen <= 0x3f) {//63
            if (!p) return len;//1
            buf[0] = ZIP_STR_06B | rawlen;
        } else if (rawlen <= 0x3fff) {//16383
            len += 1;//2
            if (!p) return len;
            buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
            buf[1] = rawlen & 0xff;
        } else {
            len += 4;//5
            if (!p) return len;
            buf[0] = ZIP_STR_32B;
            buf[1] = (rawlen >> 24) & 0xff;
            buf[2] = (rawlen >> 16) & 0xff;
            buf[3] = (rawlen >> 8) & 0xff;
            buf[4] = rawlen & 0xff;
        }

    // 编码整数
    } else {
        /* Implies integer encoding, so length is always 1. */
        if (!p) return len;
        buf[0] = encoding;
    }

    /* Store this length at p */
    // 将编码后的长度写入 p 
    memcpy(p,buf,len);

    // 返回编码所需的字节数
    return len;
}

插入元素

主要过程是:
1.确定插入位置:头插/尾插。
分为头部和尾部来讨论,
如果在头部插入,则待插入元素所需要的字节数就是头部元素prelen的值。
如果在尾部插入,则尾部元素的字节长度就是p节点的prelen长度。
2.尝试将字符串转换为长整型,比如“123”->123
转换成功将根据encoding获取不同编码获取不同大小例如uint_32位4个字节,否则使用字符串原始长度,例如“hello”长度为5个字节,将这个长度值加到reqlen上,reqlen为新添加节点的长度,包括content的长度和header的长度。
3.将prelen编码长度加到reqlen上
4.将encoding编码长度加到reqlen上
5.之后就是最复杂的一步了,就考虑如果在头部插入元素,且原来头部元素的prelen不够编码新的元素,他们之间产生一个差值,这个差值也要计算到重新分配ziplist时的大小。
比如,原节点的prelen为1个字节(记录content小于254的情况),现在插入一个很长的节点,则需要5个字节的空间保存,那么这个额外多出来的4个字节nextdiff则需要记录进来。
6.重新申请ziplist大小,为原长度curlen+新节点长度reqlen+第5步所说的nextdiff。
7.调整原来的元素,为新元素挪动位置。
8.将header写入新元素地址,此时需要挪动指针,方便拷贝content。这部分想了很久:

 // 一切搞定,将前置节点的长度写入新节点的 header
    p += zipPrevEncodeLength(p,prevlen);
    // 将节点值的长度写入新节点的 header
    p += zipEncodeLength(p,encoding,slen);
//这里写入同时还要移动p的位置,方便后面memcpy拷入content

    // 写入节点值
    if (ZIP_IS_STR(encoding)) {
        // T = O(N)
        memcpy(p,s,slen);
    } else {
        // T = O(1)
        zipSaveInteger(p,value,encoding);
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值