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

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:想要详细了解dictEntryredisObject对象以及编码相关知识的的可以点击这里

ziplist


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个字节就是记录了当前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
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

就写到这了,也算是给这段时间的面试做一个总结,查漏补缺,祝自己好运吧,也希望正在求职或者打算跳槽的 程序员看到这个文章能有一点点帮助或收获,我就心满意足了。多思考,多问为什么。希望小伙伴们早点收到满意的offer! 越努力越幸运!

金九银十已经过了,就目前国内的面试模式来讲,在面试前积极的准备面试,复习整个 Java 知识体系将变得非常重要,可以很负责任的说一句,复习准备的是否充分,将直接影响你入职的成功率。但很多小伙伴却苦于没有合适的资料来回顾整个 Java 知识体系,或者有的小伙伴可能都不知道该从哪里开始复习。我偶然得到一份整理的资料,不论是从整个 Java 知识体系,还是从面试的角度来看,都是一份含技术量很高的资料。

三面蚂蚁核心金融部,Java开发岗(缓存+一致性哈希+分布式)

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
到这个文章能有一点点帮助或收获,我就心满意足了。多思考,多问为什么。希望小伙伴们早点收到满意的offer! 越努力越幸运!**

金九银十已经过了,就目前国内的面试模式来讲,在面试前积极的准备面试,复习整个 Java 知识体系将变得非常重要,可以很负责任的说一句,复习准备的是否充分,将直接影响你入职的成功率。但很多小伙伴却苦于没有合适的资料来回顾整个 Java 知识体系,或者有的小伙伴可能都不知道该从哪里开始复习。我偶然得到一份整理的资料,不论是从整个 Java 知识体系,还是从面试的角度来看,都是一份含技术量很高的资料。

[外链图片转存中…(img-YYFqdzLx-1712255125903)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值