本次面试答案,以及收集到的大厂必问面试题分享:
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个字节就是记录了当前ziplist
中entry
的数量,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个字节,那么此时entry2
的prelen
属性就必须要由1个字节变为5个字节,也就是需要执行空间重分配,而此时因为entry2
长度也增加了4个字节,又大于254个字节了了,那么entry3
的prelen
属性也会被迫变为5个字节,依此类推,这种产生连续多次空间重分配的现象就称之为连锁更新。
PS:不仅仅是新增节点,执行删除节点操作同样可能会发生连锁更新现象。
在Redis3.2之前,linkedlist
和ziplist
两种编码可以进选择切换,如果需要列表使用ziplist
编码进行存储,则必须满足以下两个条件:
-
列表对象保存的所有字符串元素的长度都小于64字节。
-
列表对象保存的元素数量小于512个。
一旦不满足这两个条件的任意一个,则会使用linkedlist
编码进行存储。
PS:这两个条件可以通过参数list-max-ziplist-value
和list-max-ziplist-entries
进行修改。
在Redis3.2之后,统一用quicklist来存储列表对象,quicklist
存储了一个双向列表,每个列表的节点是一个ziplist
,所以实际上quicklist就是linkedlist
和ziplist
的结合。
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指针会指向另一个对象quicklistLZF
,quicklistLZF
是一个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种常用数据类型中的列表类型
底层的存储结构,并分别对其两种底层数据linkedlist
和ziplist
进行了分析对比,最后分析了Redis3.2之后的底层数据类型quicklist
的存储原理。
下一篇,我们将分析Redis中5种常用数据类型中的第3种哈希对象的底层存储结构。
请关注我,和孤狼一起学习进步。
总结
这个月马上就又要过去了,还在找工作的小伙伴要做好准备了,小编整理了大厂java程序员面试涉及到的绝大部分面试题及答案,希望能帮助到大家
孤狼一起学习进步**。
总结
这个月马上就又要过去了,还在找工作的小伙伴要做好准备了,小编整理了大厂java程序员面试涉及到的绝大部分面试题及答案,希望能帮助到大家
[外链图片转存中…(img-E59UcfCF-1715481188085)]
[外链图片转存中…(img-kmp9kDfT-1715481188085)]