listNode *head;//头节点
listNode *tail;//尾节点
void *(*dup)(void *ptr);//节点值复制函数
void (*free)(void *ptr);//节点值释放函数
int (*match)(void *ptr, void *key);//节点值对比函数
unsigned long len;//节点数量
} list;
Redis中对linkedlist
的访问是以NULL值为终点的,因为head节点的prev节点为NULL,tail节点的next节点为NULL。
所以,同样的,在Redis3.2之前我们可以得到如下简图:
PS:想要详细了解dictEntry
和redisObject
对象以及编码相关知识的的可以点击这里。
ziplist
是为了节省内存而开发的一种压缩列表数据结构,后面讲述的哈希数据类型底层也用到了ziplist
。
ziplist
是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个ziplist
可以包含任意多个entry
,而每一个entry
又可以保存一个字节数组或者一个整数值。
ziplist和linkedlist最大的区别是ziplist不存储指向上一个节点和下一个节点的指针,存储的是上一个节点的长度和当前节点的长度,牺牲了部分读写性能来换取高效的内存利用率,是一种时间换空间的思想。
ziplist
适用于字段个数少和字段值少的场景。
ziplist存储结构
ziplist
的组成结构为:
…
| 属性 | 类型 | 长度 | 说明 |
| — | — | — | — |
| zlbytes | uint32_t | 4字节 | 记录压缩列表占用内存字节数(包括本身所占用的4个字节) |
| zltail | uint32_t | 4字节 | 记录压缩列表尾节点距离压缩列表的起始地址有多少个字节(通过这个值可以计算出尾节点的地址) |
| zllen | uint16_t | 2字节 | 记录压缩列表中包含的节点数量,当列表值超过可以存储的最大值(65535)时,次值固定存储216-1(65535),因此此时需要遍历整个压缩列表才能计算出真实节点数 |
| entry | 列表节点 | - | 压缩列表中的各个节点,长度由存储的实际数据决定 |
| zlend | uint8_t | 1字节 | 特殊字符0xFF(十进制255),用来标记压缩列表的末端(其他正常的节点没有被标记为255的,因为255用来标识末尾,后面可以看到,正常节点都是标记为254) |
entry存储结构
ziplist
中的每个 entry
都以包含两段信息的元数据作为前缀,每一个 entry
的组成结构为:
prevlen
prevlen
属性存储了前一个entry
的长度,以便能够从后到前遍历列表。 prevlen
的长度可能是1字节也可能是5字节:
- 当链表的前一个
entry
占用字节数小于254,此时prevlen
只用1个字节进行表示。
<prevlen from 0 to 253>
- 当链表的前一个
entry
占用字节数大于等于254,此时prevlen
用5个字节来表示,其中第1个字节的值是254(相当于是一个标记,代表后面跟了一个更大的值),后面4个字节才是真正存储前一个entry
的占用字节数。
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
:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://img-blog.csdnimg.cn/img_convert/7f0d6592215584869d5ce304a1828fa1.jpeg)
总结
就写到这了,也算是给这段时间的面试做一个总结,查漏补缺,祝自己好运吧,也希望正在求职或者打算跳槽的 程序员看到这个文章能有一点点帮助或收获,我就心满意足了。多思考,多问为什么。希望小伙伴们早点收到满意的offer! 越努力越幸运!
金九银十已经过了,就目前国内的面试模式来讲,在面试前积极的准备面试,复习整个 Java 知识体系将变得非常重要,可以很负责任的说一句,复习准备的是否充分,将直接影响你入职的成功率。但很多小伙伴却苦于没有合适的资料来回顾整个 Java 知识体系,或者有的小伙伴可能都不知道该从哪里开始复习。我偶然得到一份整理的资料,不论是从整个 Java 知识体系,还是从面试的角度来看,都是一份含技术量很高的资料。
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!
到这个文章能有一点点帮助或收获,我就心满意足了。多思考,多问为什么。希望小伙伴们早点收到满意的offer! 越努力越幸运!**
金九银十已经过了,就目前国内的面试模式来讲,在面试前积极的准备面试,复习整个 Java 知识体系将变得非常重要,可以很负责任的说一句,复习准备的是否充分,将直接影响你入职的成功率。但很多小伙伴却苦于没有合适的资料来回顾整个 Java 知识体系,或者有的小伙伴可能都不知道该从哪里开始复习。我偶然得到一份整理的资料,不论是从整个 Java 知识体系,还是从面试的角度来看,都是一份含技术量很高的资料。
[外链图片转存中…(img-YYFqdzLx-1712255125903)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!