Redis底层之List数据结构底层原理分析

1,082 阅读5分钟

1:Redis链表实现的特性

 双端:链表节点带有 prev 和 next 指针,获取某个节点的前置节点和后置节点复杂度都是O(1)。
 ⽆环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以NULL为终点。
 带表头指针和表尾指针:通过list结构的 head 和 tail 指针,程序获取链表的表头节点和表尾结点的复杂度都是O(1)。
 带链表⻓度计数器:程序使⽤ list 结构的 len属性对 list持有的链表节点进⾏计数,程序获取链表中节点数量的复杂度为O(1)。
 多态:链表节点使⽤ void* 指针来保存节点值,并且通过 list 结构的 dup、 free、match 三个属性为节点值设置类型特定函数,所以链表可以⽤于保存各种不同类型的值。

2: 应⽤场景

lpush+lpop=Stack(栈)
lpush+rpop=Queue(队列)
lpush+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列)
排⾏榜,数据最新列表等等
集合设置过期,只能给整个集合设置,不能单独给某⼀个元素设置,没有给单独元素设置过期时间的策略

3:存储⽅式

 其底层有linkedList、zipList和quickList这三种存储⽅式

1:linkedList

 与Java中的LinkedList类似,Redis中的linkedList是⼀个双向链表,也是由⼀个个节点组成的。Redis中借助C语⾔实现的链表节点结构如下所示

image.png

 pre指向前⼀个节点,next指针指向后⼀个节点,value保存着当前节点对应的数据对象。listNode的示意图如下所示

image.png

链表的结构如下:

image.png

 head指向链表的头节点
 tail指向链表的尾节点
 dup函数⽤于链表转移复制时对节点value拷⻉的⼀个实现,⼀般情况下使⽤等号⾜以,但在某些特殊情况下可能会⽤到节点转移函数,默认可以给这个函数赋值NULL即表示使⽤等号进⾏节点转移
 free函数⽤于释放⼀个节点所占⽤的内存空间,默认赋值NULL的话,即使⽤Redis⾃带的zfree函数进⾏内存空间释放
 match函数是⽤来⽐较两个链表节点的value值是否相等,相等返回1,不等返回0  len表示这个链表共有多少个节点,这样就可以在O(1)的时间复杂度内获得链表的⻓度

链表的结构如下:

image.png

2:zipList类型

Redis的zipList结构如下所示

image.png

zipList的结构如下所示

image.png

 注意到zltail_offffset这个参数,有了这个参数就可以快速定位到最后⼀个entry节点的位置,然后开始倒序遍历,也就是说zipList⽀持双向遍历。

3:linkList与zipList的对⽐

 1:当列表对象中元素的⻓度较⼩或者数量较少时,通常采⽤zipList来存储;当列表中元素的⻓度较⼤或者数量⽐较多的时候,则会转⽽使⽤双向链表linkedList来存储。  ①列表对象保存的所有字符串元素的⻓度都⼩于64字节  ②列表元素保存的元素数量⼩于512个  2:双向链表linkedList便于在表的两端进⾏push和pop操作,在插⼊节点上复杂度很低,但是它的内存开销⽐较⼤。⾸先,它在每个节点上除了要保存数据之外,还有额外保存两个指针;其次,双向链表的各个节点都是单独的内存块,地址不连续,容易形成内存碎⽚。  3:zipList存储在⼀块连续的内存上,所以存储效率很⾼。但是它不利于修改操作,插⼊和删除操作需要频繁地申请和释放内存。特别是当zipList⻓度很⻓时,⼀次realloc可能会导致⼤量的数据拷⻉。  4:为什么有了linkedList还有设计⼀个zipList呢?就像zipList的名字⼀样,它是⼀个压缩列表,是为了节约内存⽽开发的。相⽐于linkedList,其少了pre和next两个指针。在Redis中,pre和next指针就要占⽤16个字节(64位系统的⼀个指针就是8个字节)。另外,linkedList的每个节点的内存都是单独分配,加剧内存的碎⽚化,影响内存的管理效率。与之相对的是,zipList是由连续的内存组成的,这样⼀来,由于内存是连续的,就减少了许多内存碎⽚和指针的内存占⽤,进⽽节约了内存。

4:quickList

1:起源

 在Redis3.2版本之后,list的底层实现⽅式⼜多了⼀种,quickList。qucikList是由zipList和双向链表linkedList组成的混合体。它将linkedList按段切分,每⼀段使⽤zipList来紧凑存储,多个zipList之间使⽤双向指针串接起来。

 示意图如下所示:

image.png

2:每个zipList可以存储多少个元素

redis.conf⽂件,在DVANCED CONFIG下⾯有着清晰的记载

image.png quickList内部默认单个zipList⻓度为8k字节,即list-max-ziplist-size的值设置为-2,超出了这个阈值,就会重新⽣成⼀个zipList来存储数据。

根据注释可知,性能最好的时候就是就是list-max-ziplist-size为-1和-2,即分别是4kb和8kb的时候,当然,这个值也可以被设置为正数,当list-max-ziplist-szie为正数n时,表示每个quickList节点上的zipList最多包含n个数据项。

4:⼩结

1:linkedList

1:linkedList便于在表的两端进⾏ push 和 pop 操作,但是它的内存开销⽐较⼤
2:linkedList每个节点上除了要保存数据之外,还要额外保存两个指针
3:linkedList的各个节点是单独的内存块,地址不连续,节点多了容易产⽣内存碎⽚

2:ziplist

1:ziplist 由于是⼀整块连续内存,所以存储效率很⾼
2:ziplist 不利于修改操作,每次数据变动都会引发⼀次内存的 realloc
3:当 ziplist ⻓度很⻓的时候,⼀次 realloc 可能会导致⼤批量的数据拷⻉,进⼀步降低性能

3:quicklist

1:空间效率和时间效率的折中
2:结合了双端链表和压缩列表的优点