Redis数据结构——压缩列表

前言

同整数集合一样压缩列表也不是基础数据结构,而是 Redis 自己设计的一种数据存储结构。它有点儿类似数组,通过一片连续的内存空间,来存储数据。不过,它跟数组不同的一点是,它允许存储的数据大小不同。

一、压缩列表

听到“压缩”两个字,直观的反应就是节省内存。之所以说这种存储结构节省内存,是相较于数组的存储思路而言的。我们知道,数组要求每个元素的大小相同,如果我们要存储不同长度的字符串,那我们就需要用最大长度的字符串大小作为元素的大小(假设是20个字节)。存储小于 20 个字节长度的字符串的时候,便会浪费部分存储空间。

数组的优势占用一片连续的空间可以很好的利用CPU缓存访问数据。如果我们想要保留这种优势,又想节省存储空间我们可以对数组进行压缩。

但是这样有一个问题,我们在遍历它的时候由于不知道每个元素的大小是多少,因此也就无法计算出下一个节点的具体位置。这个时候我们可以给每个节点增加一个lenght的属性。

如此。我们在遍历节点的之后就知道每个节点的长度(占用内存的大小),就可以很容易计算出下一个节点再内存中的位置。这种结构就像一个简单的压缩列表了。

二、Redis压缩列表

压缩列表(ziplist)是列表和哈希的底层实现之一。

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

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

ziplist存储结构

typedf struct ziplist{
    //压缩列表占用字符数
    int32 zlbytes;
    
    //最后一个元素距离起始位置的偏移量,用于快速定位最后一个节点
    int32 zltail_offset;
    
    //元素个数
    int16 zllength;
    
    //元素内容
    T[] entries;
    
    //结束位 0xFF
    int8 zlend;
}ziplist;

各个部分在内存上是前后相邻的并连续的,每一部分作用如下:

  • zlbytes: 存储一个无符号整数,固定四个字节长度(32bit),用于存储压缩列表所占用的字节(也包括本身占用的4个字节),当重新分配内存的时候使用,不需要遍历整个列表来计算内存大小。

  • zltail: 存储一个无符号整数,固定四个字节长度(32bit),表示ziplist表中最后一项(entry)在ziplist中的偏移字节数。的存在,使得我们可以很方便地找到最后一项(不用遍历整个ziplist),从而可以在ziplist尾端快速地执行push或pop操作。

  • zllen: 压缩列表包含的节点个数,固定两个字节长度(16bit), 表示ziplist中数据项(entry)的个数。由于zllen字段只有16bit,所以可以表达的最大值为2^16-1。

注意点:如果ziplist中数据项个数超过了16bit能表达的最大值,ziplist仍然可以表示。ziplist是如何做到的?
如果小于等于216-2(也就是不等于216-1),那么就表示ziplist中数据项的个数;否则,也就是等于16bit全为1的情况,那么就不表示数据项个数了,这时候要想知道ziplist中数据项总数,那么必须对ziplist从头到尾遍历各个数据项,才能计数出来。

  • entry:表示真正存放数据的数据项,长度不定。一个数据项(entry)也有它自己的内部结构。

  • zlend:ziplist最后1个字节,值固定等于255,其是一个结束标记。

ziplist节点entry结构

从ziplist节点的存储结构,我们可以看到zlentry结构和节点在ziplist的真实的存储结构并不是一一对应的。那么我们就来看看ziplist怎么从一段字符数组转换为zlentry结构的?

// 压缩链表结构体
// 元素实体所有信息, 仅仅是描述使用, 内存中并非如此存储
typedef struct zlentry {

    // prevrawlen为上一个链表节点占用的长度
    // prevrawlensize为存储上一个链表节点的长度数值所需要的字节数
    unsigned int prevrawlensize, prevrawlen;

    // len为当前链表节点占用的长度
    // lensize为存储当前链表节点长度数值所需要的字节数
    unsigned int lensize, len;
    
    // 当前链表节点的头部大小(prevrawlensize + lensize),即非数据域的大小
    unsigned int headersize;

    // 元素内容编码方式
    unsigned char encoding;

    //元素实际内容
    // 压缩链表以字符串的形式保存,该指针指向当前节点起始位置
    unsigned char *p;

} zlentry;

为了便于理解我们可以

  • 将第一部分prev_entry_length域看做对prevrawlensize、prevrawlen字段的抽象;
  • 将第二部分cur_entry_length域看做是对lensize、len字段的抽象。
  • 另外,我们经常需要跳过节点的header部分(第一部分和第二部分)读取节点真正存储的数据,所以zlentry结构定义了headersize字段记录节点头部长度。

整个ziplist的存储结构可以总结如下:


每个数据项entry由三部分构成:

struct entry{
    //前一个 entry 的长度
    int prevlen;
    
    //元素类型编码
    int encoding;

    //元素内容  
    optional byte[] content;
}

2.1 Redis压缩列表的构成

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


示例:


如上图,展示了一个总长为80字节,包含3个节点的压缩列表。如果我们有一个指向压缩列表起始地址的指针p,那么表为节点的地址就是P+60。

2.2 Redis压缩列表节点的构成

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

  • 长度小于等于63(2^6-1)字节的字节数组。
  • 长度小于等于16383(2^14-1)字节的字节数组。
  • 长度小于等于4294967295(2^32-1)字节的字节数组。

整数值可以是以下6种长度中的一种:

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

节点的 previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。 previous_entry_length属性的长度可以是1字节或者5字节。

  • 如果前一节点的长度小于254字节,那么 previous_entry_length属性的长度为1字节,前一节点的长度就保存在这一个字节里面。

  • 如果前一节点的长度大于等于254字节,那么 previous_entry_length属性的长度为5字节:其中属性的第一字节会被设置为0xFE(十进制值254),而之后的四个字节则用于保存前一节点的长度。

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

  • 一字节、两字节或者五字节长,值的最高位为00、01或者10的是字节数组编码这种编码表示节点的 content属性保存着字节数组,数组的长度由编码除去最高两位之后的其他位记录。

  • 一字节长,值的最高位以11开头的是整数编码:这种编码表示节点的content属性保存着整数值,整数值的类型和长度由编码除去最高两位之后的其他位记录。

节点的content属性负责保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。

  • 编码的最高两位00表示节点保存的是一个字节数组。
  • 编码的后六位001011记录了字节数组的长度11。
  • content属性保存着节点的值"hello world"。
  • 编码11000000表示节点保存的是一个int16_t类型的整数值。
  • content属性保存着节点的值10086。

2.3 常用操作的时间复杂度

操作 时间复杂度
创建一个新的压缩列表 O(1)
创建一个包含给定值的新节点,并将这个新节点添加到压缩列表的表头或者表尾 平均O(N),最坏O(N^2)(可能发生连锁更新)
将包含给定值的新节点插人到给定节点之后 平均O(N),最坏O(N^2)(可能发生连锁更新)
返回压缩列表给定索引上的节点 O(N)
在压缩列表中査找并返回包含了给定值的节点 因为节点的值可能是一个字节数组,所以检查节点值和给定值是否相同的复杂度为O(N),而查找整个列表的复杂度则为(N^2)
返回给定节点的下一个节点 O(1)
返回给定节点的前一个节点 O(1)
获取给定节点所保存的值 O(1)
从压缩列表中删除给定的节点 平均O(N),最坏O(N^2)(可能发生连锁更新)
删除压缩列表在给定索引上的连续多个 平均O(N),最坏O(N^2)(可能发生连锁更新)
返回压缩列表目前占用的内存字节数 O(1)
返回压缩列表目前包含的节点数量 点数量小于65535时为O(1),大于65535时为O(N)

本文重点

  • 压缩列表是Redis为节约内存自己设计的一种顺序型数据结构。
  • 压缩列表被用作列表键和哈希键的底层实现之一。
  • 压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。
  • 添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率并不高。

参考:
https://www.cnblogs.com/hunternet/p/11306690.html

https://www.cnblogs.com/yinbiao/p/11253950.html

https://www.cnblogs.com/exceptioneye/p/7040815.html

https://www.cnblogs.com/williamjie/p/9502626.html

https://www.cnblogs.com/reecelin/p/13358432.html

你可能感兴趣的:(Redis数据结构——压缩列表)