Redis源码初探(4)压缩列表ziplist

压缩列表

压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。

压缩列表的构成

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。

area        |<---- ziplist header ---->|<--- entries -->|<-- end -->|

size          4 bytes  4 bytes  2 bytes  5 bytes          1 bytes
            +---------+--------+-------+----------------+-----------+
component   | zlbytes | zltail | zllen | entry 1        | zlend     |
            |         |        |       |                |           |
value       |  10000  |  1010  |   1   | ?              | 1111 1111 |
            +---------+--------+-------+----------------+-----------+
                                       ^                ^
                                       |                |
address                         ZIPLIST_ENTRY_HEAD   ZIPLIST_ENTRY_END
                                       &
                                ZIPLIST_ENTRY_TAIL
  • zlbytes记录整个压缩列表占用的内存字节数。
  • zltail记录压缩列表表尾节点距离压缩列表的起始地址有多少个字节。
  • zllen记录压缩列表的节点数量。
  • entryX为压缩列表包含的各个节点,节点的长度由节点保存的内容决定。
  • zlend特殊值0XFF,用于标记压缩列表的末端。

同时使用zlentry来保存ziplist节点的信息,要注意的是,zlentry并不是上文中的entry

typedef struct zlentry {
    // prevrawlen :前置节点的长度
    // prevrawlensize :编码 prevrawlen 所需的字节大小
    unsigned int prevrawlensize, prevrawlen;
    // len :当前节点值的长度
    // lensize :编码 len 所需的字节大小
    unsigned int lensize, len;
    // 当前节点 header 的大小
    // 等于 prevrawlensize + lensize
    unsigned int headersize;
    // 当前节点值所使用的编码类型
    unsigned char encoding;
    // 指向当前节点的指针
    unsigned char *p;
} zlentry;

压缩列表节点的构成

每个压缩列表节点可以保存一个字节数组或者一个整数值,其中,字节数组可以是以下三种长度的其中一种:

  • 长度小于等于2的6次方-1字节的字节数组。
  • 长度小于等于2的14次方-1字节的字节数组。
  • 长度小于等于2的32次方-1字节的字节数组。

而整数值则可以是以下六种长度的其中一种:

  • 4位长,介于0至12之间的无符号整数。
  • 1字节长的有符号整数。
  • 3字节长的有符号整数。
  • int16_t类型整数。
  • int32_t类型整数。
  • int64_t类型的整数。

每个压缩列表节点都由previous_entry_length、encoding、content三个属性构成。

previous_entry_length从名字就可以看出,它记录了压缩列表前一个节点的长度。该属性可以是1字节或者5字节,当前一个字节长度小于254字节时,该属性就用1字节保存前节点的长度;反之则使用5字节保存前节点的长度,需要注意的是这五个字节的第一字节设置为0XFE(54),之后的四位用来保存前节点长度。

encoding属性记录了节点的content属性所保存数据的类型以及长度:

在这里插入图片描述

  • 当data小于63字节时(2^6),节点存为上图的第一种类型,高2位为00,低6位表示data的长度。
  • 当data小于16383字节时(2^14),节点存为上图的第二种类型,高2位为01,后续14位表示data的长度。
  • 当data小于4294967296字节时(2^32),节点存为上图的第二种类型,高2位为10,下一字节起连续32位表示data的长度。

连锁更新

因为压缩列表每个节点的previous_entry_length属性都记录了前一个节点的长度,并且该属性会使用1字节或5字节的空间,那么如果有很多个节点,并且他们的长度都在250到254之间,那么如果在第一个节点之前添加一个长度大于254的节点,那么原来的第一个节点的previous_entry-length就需要变成五个字节,那么该节点的长度就超过了254,那么原来的第二个节点的previous_entry-length也要跟着变化,这么一来程序需要频繁的对压缩列表节点进行空间重分配,Redis将在这种情况下产生的连续多次空间扩展称之为“连锁更新”。当然,除了添加节点以外,删除节点也会引发连锁更新,这种情况还是很容易想象出来的,这里就不再细说了。

因为连锁更新最坏情况下需要对压缩列表所有的N个节点进行空间重分配,每次空间重分配的最坏时间复杂度为O(N),所以连锁更新的最坏时间复杂度为O(N方)。

虽然连锁更新的复杂度较高,但是真正会产生连锁更新的情况并不会常发生,所以可以放心使用。

源码分析

压缩列表的创建:

unsigned char *ziplistNew(void) {
    unsigned int bytes = ZIPLIST_HEADER_SIZE+1;//<zlbytes>4字节<zltail>4字节<zllen>2字节<zlend>1字节,没有entry节点
    unsigned char *zl = zmalloc(bytes);
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);//<zlbytes>赋值
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);//<zltail>
    ZIPLIST_LENGTH(zl) = 0;//<zllen>
    zl[bytes-1] = ZIP_END;//<zlend>
    return zl;
}
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))//空ziplist除了<zlend>的大小
#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))//<zlbyte>的指针的值,可读可写
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))//<zltail>的指针的值
#define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))//空ziplist除了<zlend>的大小
#define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))//<zllen>的指针的值

分配了一块内存并初始化,没有初始化entry。

压缩列表的查找:

该方法的作用是寻找节点值和 vstr 相等的列表节点,并返回该节点的指针。

unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip) {
    int skipcnt = 0;
    unsigned char vencoding = 0;
    long long vll = 0;

    while (p[0] != ZIP_END) {
        unsigned int prevlensize, encoding, lensize, len;
        unsigned char *q;

        ZIP_DECODE_PREVLENSIZE(p, prevlensize);
        ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
        q = p + prevlensize + lensize;//当前节点的data

        if (skipcnt == 0) {
            /* Compare current entry with specified entry */
            if (ZIP_IS_STR(encoding)) {//判断当前节点是不是字符串节点
                if (len == vlen && memcmp(q, vstr, vlen) == 0) {
                    return p;
                }
            } else {
                /* Find out if the searched field can be encoded. Note that
                 * we do it only the first time, once done vencoding is set
                 * to non-zero and vll is set to the integer value. */
                if (vencoding == 0) {//这个代码块只会执行一次,计算vstr的整数表示
                    if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {
                        //将参数给的节点vstr当做整数节点转换;将data值返回给vll,节点编码返回给vencoding
                        //进入这个代码块说明将vstr转换成整数失败,vencoding不变,下次判断当前节点是整数节点之后可以跳过这个节点
                        /* If the entry can't be encoded we set it to
                         * UCHAR_MAX so that we don't retry again the next
                         * time. */
                        vencoding = UCHAR_MAX;//当前节点是整数节点,但是vstr是字符串节点,跳过不用比较了
                    }
                    /* Must be non-zero by now */
                    assert(vencoding);
                }

                /* Compare current entry with specified entry, do it only
                 * if vencoding != UCHAR_MAX because if there is no encoding
                 * possible for the field it can't be a valid integer. */
                if (vencoding != UCHAR_MAX) {
                    long long ll = zipLoadInteger(q, encoding);//算出当前节点的data
                    if (ll == vll) {
                        return p;
                    }
                }
            }

            /* Reset skip count */
            skipcnt = skip;
        } else {
            /* Skip entry */
            skipcnt--;
        }

        /* Move to next entry */
        p = q + len;
    }

    return NULL;
}

//尝试将entry地址的内容转换成整数,并根据这个整数算出一个合适的encoding返回给encoding参数。
//若无法转换成整数,则encoding不变,返回0,等到下次调用zipEncodeLength时再计算一个该字符串的encoding
int zipTryEncoding(unsigned char *entry, unsigned int entrylen, long long *v, unsigned char *encoding) {
    long long value;

    if (entrylen >= 32 || entrylen == 0) return 0;
    if (string2ll((char*)entry,entrylen,&value)) {
        /* Great, the string can be encoded. Check what's the smallest
         * of our encoding types that can hold this value. */
        if (value >= 0 && value <= 12) {
            *encoding = ZIP_INT_IMM_MIN+value;
        } else if (value >= INT8_MIN && value <= INT8_MAX) {
            *encoding = ZIP_INT_8B;
        } else if (value >= INT16_MIN && value <= INT16_MAX) {
            *encoding = ZIP_INT_16B;
        } else if (value >= INT24_MIN && value <= INT24_MAX) {
            *encoding = ZIP_INT_24B;
        } else if (value >= INT32_MIN && value <= INT32_MAX) {
            *encoding = ZIP_INT_32B;
        } else {
            *encoding = ZIP_INT_64B;
        }
        *v = value;
        return 1;
    }
    return 0;
}

/* Read integer encoded as 'encoding' from 'p' */
int64_t zipLoadInteger(unsigned char *p, unsigned char encoding) {
    int16_t i16;
    int32_t i32;
    int64_t i64, ret = 0;
    if (encoding == ZIP_INT_8B) {
        ret = ((int8_t*)p)[0];
    } else if (encoding == ZIP_INT_16B) {
        memcpy(&i16,p,sizeof(i16));
        memrev16ifbe(&i16);
        ret = i16;
    } else if (encoding == ZIP_INT_32B) {
        memcpy(&i32,p,sizeof(i32));
        memrev32ifbe(&i32);
        ret = i32;
    } else if (encoding == ZIP_INT_24B) {
        i32 = 0;
        memcpy(((uint8_t*)&i32)+1,p,sizeof(i32)-sizeof(uint8_t));
        memrev32ifbe(&i32);
        ret = i32>>8;
    } else if (encoding == ZIP_INT_64B) {
        memcpy(&i64,p,sizeof(i64));
        memrev64ifbe(&i64);
        ret = i64;
    } else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) {
        ret = (encoding & ZIP_INT_IMM_MASK)-1;
    } else {
        assert(NULL);
    }
    return ret;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

kinron_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值