02-Redis数据结构-List

Redis列表list 底层原理
在这里插入图片描述
在版本3.2之前,Redis 列表list使用两种数据结构作为底层实现:

  1. 压缩列表ziplist
  2. 双向链表linkedlist

因为双向链表占用的内存比压缩列表要多, 所以当创建新的列表键时, 列表会优先考虑使用压缩列表, 并且在有需要的时候, 才从压缩列表实现转换到双向链表实现。

压缩列表转化成双向链表
创建新列表时 redis 默认使用 redis_encoding_ziplist 编码, 当以下任意一个条件被满足时, 列表会被转换成redis_encoding_linkedlist 编码:

  1. 单字符串的值长度超过 server.list_max_ziplist_value (默认值为 64 )。
  2. ziplist 包含的节点超过 server.list_max_ziplist_entries (默认值为 512 )。

注意:这两个条件是可以修改的,在 redis.conf 中:

list-max-ziplist-value 64 
list-max-ziplist-entries 512 

压缩列表ziplist

压缩列表 ziplist 是为 Redis 节约内存而开发的。

  1. ziplist 是由一系列特殊编码的内存块构成的列表(像内存连续的数组,但每个元素长度不同), 一个 ziplist
    可以包含多个节点(entry)。
  2. ziplist 将表中每一项存放在前后连续的地址空间内,每一项因占用的空间不同,而采用变长编码。

由于内存是连续分配的,所以遍历速度很快。在3.2之后,ziplist被quicklist替代。但是仍然是zset底层实现之一。

ziplist 是一个特殊的双向链表
特殊之处在于:没有维护双向指针:prev next;而是存储上一个 entry的长度当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度 更费内存。这是典型的"时间换空间"。

压缩列表ziplist存储结构
ziplist使用连续的内存块,每一个节点(entry)都是连续存储的;ziplist 存储分布如下:

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

size          4 bytes  4 bytes  2 bytes    ?        ?        ?        ?     1 byte
            +---------+--------+-------+--------+--------+--------+--------+-------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | entryN | zlend |
            +---------+--------+-------+--------+--------+--------+--------+-------+
                                       ^                          ^        ^
address                                |                          |        |
                                ZIPLIST_ENTRY_HEAD                |   ZIPLIST_ENTRY_END
                                                                  |
                                                         ZIPLIST_ENTRY_TAIL

第一行:整个数据结构是header-entries-end
第二行:component行每个小空间最占用的字节数。
第三行:对ziplist每块空间更细的划分

图中各个域的含义如下:
在这里插入图片描述
其中entries才是ziplist真正存储节点的区域,每一项都连续存储的,构成entry1、entry2、….entryN。
由于每一项、占用的空间可能都不同,只能在添加entry的时候确定,也就是每个entry占用的实际长度只有在节点添加后才确定,因此entry采用变长编码

ziplist节点entry结构
每一个存储节点(entry)都是一个zlentry (zip list entry)。
在这里插入图片描述
ziplist每一个存储节点、都是一个 zlentry,就是上文我们所说的entry;
zlentry的源码定义:

typedef struct zlentry {         // 压缩列表节点
    unsigned int prevrawlensize; //prevrawlensize是指prevrawlen的大小,有1字节和5字节两种
    unsigned int prevrawlen;     //prevrawlen是前一个节点的长度
    unsigned int lensize, 		 //lensize为编码len所需的字节大小
    unsigned int len;   		 // len为当前节点长度 
    unsigned int headersize;     // 当前节点的header大小
    unsigned char encoding;      // 节点的编码方式
    unsigned char *p;            // 指向节点的指针
} zlentry;

每一个存储节点 zlentry,都包含

  1. prevrawlen 前一个 entry的长度
  2. lensize 当前entry的长度

作用:当前节点的指针 e 减去前一个entry的长度,得出的结果就是指向前一个节点的地址p

void zipEntry(unsigned char *p, zlentry *e) {   // 根据节点指针返回一个enrty
    ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);    // 获取prevlen的值和长度
    ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);  // 获取当前节点的编码方式、长度等
    e->headersize = e->prevrawlensize + e->lensize; // 头大小
    e->p = p;
}

完整的zlentry由以下3各部分组成:

  1. prevrawlen:记录前一个节点所占有的内存字节数,通过该值,我们可以从当前节点计算前一个节点的地址,可以用来实现表尾向表头节点遍历;
  2. len/encoding:记录了当前节点content占有的内存字节数及其存储类型,用来解析content用;
  3. content:保存了当前节点的值。

最关键的是prevrawlen和len/encoding,content只是实际存储数值的比特位。

prevrawlen是变长编码,有两种表示方法:

  1. 如果前一节点的长度小于 254 字节,则使用1字节(uint8_t)来存储prevrawlen;
  2. 如果前一节点的长度大于等于 254 字节,那么将第 1 个字节的值设为 254 ,然后用接下来的 4 个字节保存实际长度。

ziplist使用局限性
字段、值比较小,才会用ziplist。

ziplist连锁更新问题

因为在ziplist中,每个zlentry都存储着前一个节点所占的字节数,而这个数值又是变长编码的。假设存在一个压缩列表,其包含e1、e2、e3、e4……,e1节点的大小为253字节,那么e2.prevrawlen的大小为1字节,如果此时在e2与e1之间插入了一个新节点e_new,e_new编码后的整体长度(包含e1的长度)为254字节,此时e2.prevrawlen就需要扩充为5字节;如果e2的整体长度变化又引起了e3.prevrawlen的存储长度变化,那么e3也需要扩…….如此递归直到表尾节点或者某一个节点的prevrawlen本身长度可以容纳前一个节点的变化。其中每一次扩充都需要进行空间再分配操作。删除节点亦是如此,只要引起了操作节点之后的节点的prevrawlen的变化,都可能引起连锁更新。
连锁更新在最坏情况下需要进行N次空间再分配,而每次空间再分配的最坏时间复杂度为O(N),因此连锁更新的总体时间复杂度是O(N^2)。
即使涉及连锁更新的时间复杂度这么高,但它能引起的性能问题的概率是极低的:需要列表中存在大量的节点长度接近254的zlentry。
由于ziplist连锁更新的问题,也使得ziplist的优缺点极其明显;也使得后续Redis采取折中,替换了ziplist。
ziplist的主要优点是节省内存,但它上面的查找操作只能按顺序查找(可以是从前往后、也可以从后往前)。
ziplist将数据按照一定规则编码在一块连续的内存区域,目的是节省内存,这种结构并不擅长做修改操作。一旦数据发生改动,就会引发内存realloc,可能导致内存拷贝。

双向链表linkedlist

当链表entry数据超过512、或单个value 长度超过64,底层就会转化成linkedlist编码;
linkedlist是标准的双向链表,Node节点包含prev和next指针,可以进行双向遍历;
还保存了 head 和 tail 两个指针,因此,对链表的表头和表尾进行插入的复杂度都为 (1) —— 这是高效实现 LPUSH,RPOP,RPOPLPUSH 等命令的关键。

Redis中的列表list,在版本3.2之前,列表底层的编码是ziplist和linkedlist实现的,但是在版本3.2之后,重新引入 quicklist,列表的底层都由quicklist实现。可以认为quickList,是ziplist和linkedlist二者的结合;quickList将二者的优点结合起来。

quickList是一个ziplist组成的双向链表。每个节点使用ziplist来保存数据。本质上来说,quicklist里面保存着一个一个小的ziplist。结构如下:
在这里插入图片描述

typedef struct quicklistNode {
    struct quicklistNode *prev; //上一个node节点
    struct quicklistNode *next; //下一个node
    unsigned char *zl;            //保存的数据 压缩前ziplist 压缩后压缩的数据
    unsigned int sz;             /* ziplist size in bytes */
    unsigned int count : 16;     /* count of items in ziplist */
    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */
    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */
    unsigned int recompress : 1; /* was this node previous compressed? */
    unsigned int attempted_compress : 1; /* node can't compress; too small */
    unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
typedef struct quicklistLZF {
    unsigned int sz; /* LZF size in bytes*/
    char compressed[];
} quicklistLZF;
typedef struct quicklist {
    quicklistNode *head; //头结点
    quicklistNode *tail; //尾节点
    unsigned long count;        /* total count of all entries in all ziplists */
    unsigned int len;           /* number of quicklistNodes */
    int fill : 16;              /* fill factor for individual nodes *///负数代表级别,正数代表个数
    unsigned int compress : 16; /* depth of end nodes not to compress;0=off *///压缩级别
} quicklist;

quickList就是一个标准的双向链表的配置,有head 有tail;

  1. 每一个节点是一个quicklistNode,包含prev和next指针。
  2. 每一个quicklistNode 包含 一个ziplist,*zp 压缩链表里存储键值。

所以quicklist是对ziplist进行一次封装,使用小块的ziplist来既保证了少使用内存,也保证了性能。

两种存储方式的优缺点

  1. 双向链表linkedlist便于在表的两端进行push和pop操作,在插入节点上复杂度很低,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
  2. ziplist存储在一段连续的内存上,所以存储效率很高。但是,它不利于修改操作,插入和删除操作需要频繁的申请和释放内存。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝。

本文摘自:https://zhuanlan.zhihu.com/p/102422311

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值