【Redis系列3】Redis列表对象之linkedlist(双端列表)和ziplist(压缩列表)及quicklick(快速列表)实现原理分析

本次面试答案,以及收集到的大厂必问面试题分享:

字节跳动超高难度三面java程序员面经,大厂的面试都这么变态吗?

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

0xFE <4 bytes unsigned little endian prevlen>

PS:1个字节完全你能存储255,之所以只取到254是因为zlend就是固定的255,所以255这个数要用来判断是否是ziplist的结尾。

encoding

encoding属性存储了当前entry所保存数据的类型以及长度。encoding长度为1字节,2字节或者5字节长。前面我们提到,每一个entry中可以保存字节数组和整数,而encoding属性的第1个字节就是用来确定当前entry存储的是整数还是字节数组。当存储整数时,第1个字节的前两位总是11,而存储字节数组时,则可能是00、01和10。

  • 当存储整数时,第1个字节的前2位固定为11,其他位则用来记录整数值的类型或者整数值:

| 编码 | 长度 | entry保存的数据 |

| — | — | — |

| 11000000 | 1字节 | int16_t类型整数 |

| 11010000 | 1字节 | int32_t类型整数 |

| 11100000 | 1字节 | int64_t类型整数 |

| 11110000 | 1字节 | 24位有符号整数 |

| 11111110 | 1字节 | 8位有符号整数 |

| 1111xxxx | 1字节 | xxxx代表区间0001-1101,存储了一个介于0-12之间的整数,此时无需entry-data属性 |

PS:xxxx四位编码范围是0000-1111,但是0000,1111和1110已经被占用了,所以实际上的范围是0001-1101,此时能保存数据1-13,再减去1之后范围就是0-12。至于为什么要减去1个人的理解是从使用概率上来说0是很常用的一个数字,所以才会选择统一减去1来存储一个0-12的区间而不是直接存储1-13的区间。

  • 当存储字节数组时,第1个字节的前2位为00、01或者10,其他位则用来记录字节数组的长度:

| 编码 | 长度 | entry保存的数据 |

| — | — | — |

| 00pppppp | 1字节 | 长度小于等于63字节(6位)的字节数组 |

| 01pppppp qqqqqqqq | 2字节 | 长度小于等于16383字节(14位)的字节数组 |

| 10000000 qqqqqqqq rrrrrrrr ssssssss tttttttt | 5字节 | 长度小于等于232-1字节(32位)的字节数组,其中第1个字节的后6位设置为0,暂时没有用到,后面的32位(4个字节)存储了数据 |

entry-data

entry-data:具体数据。当存储小整数时,encoding就是数据本身,此时没有entry-data部分,没有entry-data部分之后的ziplist结构如下:

压缩列表中entry的数据结构定义如下(源码ziplist.c内),当然这个代码注释写了实际存储并没有用到这个结构,这个结构只是用来接收数据,所以了解一下就可以了:

typedef struct zlentry {

unsigned int prevrawlensize;//存储prevrawlen所占用的字节数

unsigned int prevrawlen;//存储上一个链表节点需要的字节数

unsigned int lensize;//存储len所占用的字节数

unsigned int len;//存储链表当前节点的字节数

unsigned int headersize;//当前链表节点的头部大小(prevrawlensize + lensize)即非数据域的大小

unsigned char encoding;//编码方式

unsigned char *p;//指向当前节点的起始位置(因为列表内的数据也是一个字符串对象)

} zlentry;

ziplist数据示例

上面讲解了这么多,听起来非常复杂,下面我们就通过一个ziplist存储整数为例子来分析一下。

[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]

| | | | | |

zlbytes zltail zllen “2” “5” end

1、第一组4个字节代表zlbytes,0f转成二进制就是1111也就是15,也就是这整个ziplist长度是15个字节。

2、第二组4个字节zltail,0c转成二进制就是1100也就是12,就是说[02 f6]这个尾节点距离起始位置有12个字节。

3、第三组2个字节就是记录了当前ziplistentry的数量,02转成二进制就是10,也就是说当前ziplist有2个节点

4、[00 f3]就是第一个entry,00表示0,因为这是第1个节点,所以前一个节点长度为0,f3转成二进制就是11110011,刚好对应了编码1111xxxx,所以后面四位就是存储了一个0-12的整数。0011转成十进制就是3,减去1得到2,所以第一个entry存储的数据就是2。后面[02 f6]一样的算法可以得出就是5。

5、最后一组1个字节[ff]转成二进制就是11111111,代表这是整个ziplist的结尾。

假如这时候又添加了一个Hello World到列表中,那么就会新增一个entry

[02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]

1、第一个字节02转成十进制就是2表示前一个节点长度是2.

2、第2个字节0b转成二进制为00001011,以00开头,符合编码00pppppp,而除掉最开始的两位00计算之后得到十进制11,这就说明后面字节数组的长度是11.

3、第三部分的11个字节就是存储了Hello World的字节数组

ziplist连锁更新问题

前面提到entry中的prevlen属性可能是1个字节也可能是5个字节,我们设想这么一种场景:假设一个ziplist中,连续多个entry的长度都是一个接近但是又不到254的值(介于250~253之间),假如这时候新增一个entry1长度增加到大于254个字节,那么此时entry2prelen属性就必须要由1个字节变为5个字节,也就是需要执行空间重分配,而此时因为entry2长度也增加了4个字节,又大于254个字节了了,那么entry3prelen属性也会被迫变为5个字节,依此类推,这种产生连续多次空间重分配的现象就称之为连锁更新

PS:不仅仅是新增节点,执行删除节点操作同样可能会发生连锁更新现象。

linkedlist和ziplist的选择


在Redis3.2之前,linkedlistziplist两种编码可以进选择切换,如果需要列表使用ziplist编码进行存储,则必须满足以下两个条件:

  • 列表对象保存的所有字符串元素的长度都小于64字节。

  • 列表对象保存的元素数量小于512个。

一旦不满足这两个条件的任意一个,则会使用linkedlist编码进行存储。

PS:这两个条件可以通过参数list-max-ziplist-valuelist-max-ziplist-entries进行修改。

quicklist


在Redis3.2之后,统一用quicklist来存储列表对象,quicklist存储了一个双向列表,每个列表的节点是一个ziplist,所以实际上quicklist就是linkedlistziplist的结合。

quicklist内部存储结构

quicklist中每一个节点都是一个quicklistNode对象,其数据结构定义为:

typedef struct quicklistNode {

struct quicklistNode *prev;//前一个节点

struct quicklistNode *next;//后一个节点

unsigned char *zl;//当前指向的ziplist或者quicklistLZF

unsigned int sz;//当前ziplist占用字节

unsigned int count : 16;//ziplist中存储的元素个数,16字节(最大65535个)

unsigned int encoding : 2; //是否采用了LZF压缩算法压缩节点 1:RAW 2:LZF

unsigned int container : 2; //存储结构,NONE=1, ZIPLIST=2

unsigned int recompress : 1; //当前ziplist是否需要再次压缩(如果前面被解压过则为true,表示需要再次被压缩)

unsigned int attempted_compress : 1;//测试用

unsigned int extra : 10; //后期留用

} quicklistNode;

然后各个quicklistNode就构成了一个列表quicklist

typedef struct quicklist {

quicklistNode *head;//列表头节点

quicklistNode *tail;//列表尾节点

unsigned long count;//ziplist中一共存储了多少元素,即:每一个quicklistNode内的count相加

unsigned long len; //双向链表的长度,即quicklistNode的数量

int fill : 16;//填充因子

unsigned int compress : 16;//压缩深度 0-不压缩

} quicklist;

根据这两个结构,我们可以得到Redis3.2之后的列表对象的一个简图:

在这里插入图片描述

quicklist的compress属性

compress表示压缩深度,可以通过参数list-compress-depth控制:

  • 0:不压缩(默认值)

  • 1:首尾第1个元素不压缩

  • 2:首位前2个元素不压缩

  • 3:首尾前3个元素不压缩

  • 以此类推

PS:之所以采取这种方式去控制是因为很多场景都是两端的元素访问率较高,而中间元素访问率相对较低。

quicklistNode的zl指针

zl指针默认指向了ziplist,sz属性记录了当前ziplist占用的字节,不过这仅仅限于当前节点没有被压缩(LZF压缩算法)的情况,如果当前节点被压缩了,那么zl指针会指向另一个对象quicklistLZFquicklistLZF是一个4+N字节的结构:

typedef struct quicklistLZF {

unsigned int sz;// LZF大小

char compressed[];//被压缩的内容

} quicklistLZF;

quicklist对比原始两种列表的改进

quicklist同样采用了linkedlist的双端列表特性,然后quicklist中的每个节点又是一个ziplist,所以quicklist就是综合平衡考虑了空间碎片和读写性能两个维度。使用quicklist需要注意以下2点:

  • 1、如果ziplist中的entry个数过少,极端情况就是只有1个entry,此时就相当于退化成了一个普通的linkedlist

  • 2、如果ziplist中的entry过多,那么也会导致一次性需要申请的内存空间过大,而且因为ziplist本身的就是以时间换空间,所以会过多entry也会影响到列表对象的读写性能。

ziplist中的entry个数可以通过参数list-max-ziplist-size来控制:

list-max-ziplist-size 1

注意:这个参数可以配置正数也可以配置负数。正数表示限制每个节点中的entry数量,如果是负数则只能为-1~-5

  • -1:每个ziplist最多只能为4KB

  • -2:每个ziplist最多只能为8KB

  • -3:每个ziplist最多只能为16KB

  • -4:每个ziplist最多只能为32KB

  • -5:每个ziplist最多只能为64KB

总结

===============================================================

本文主要介绍了Redis中5种常用数据类型中的列表类型底层的存储结构,并分别对其两种底层数据linkedlistziplist进行了分析对比,最后分析了Redis3.2之后的底层数据类型quicklist的存储原理。
下一篇,我们将分析Redis中5种常用数据类型中的第3种哈希对象的底层存储结构。
请关注我,和孤狼一起学习进步

总结

这个月马上就又要过去了,还在找工作的小伙伴要做好准备了,小编整理了大厂java程序员面试涉及到的绝大部分面试题及答案,希望能帮助到大家

在这里插入图片描述

在这里插入图片描述

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

孤狼一起学习进步**。

总结

这个月马上就又要过去了,还在找工作的小伙伴要做好准备了,小编整理了大厂java程序员面试涉及到的绝大部分面试题及答案,希望能帮助到大家

[外链图片转存中…(img-E59UcfCF-1715481188085)]

[外链图片转存中…(img-kmp9kDfT-1715481188085)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值