参考《Redis的设计与实现》
一、链表
链表提供高效的节点重排能力,以及顺序性的节点访问方式,是一种非常常用的数据数据结构,但是在C语言中并没有自己的链表实现,所以Redis自己设计了一个链表实现。
在Redis中list类型使用到了链表类型,链表中的每个节点保存了一个整数值,除此之外,发布与订阅、慢查询、监视器这些功能也使用到了链表,所以链表对于Redis来说是非常重要的结构
在adList.h/listNode下定义了链表每个节点的信息
typedef struct listNode {
struct listNode *prev; //前置节点
struct listNode *next; //后置节点
void *value; //数据
} listNode;
当然,使用C语言写链表的话同时也会具有一个将ListNode组装起来的结构,在Redis中也不意外
typedef struct list {
listNode *head; //头
listNode *tail; //尾
void *(*dup)(void *ptr); //节点复制
void (*free)(void *ptr); //节点释放函数
int (*match)(void *ptr, void *key); //节点值对比函数
unsigned long len; //包含的节点数量
} list;
那么,对于一个链表来说,Redis是如下进行存储的
总结,在Redis中的链表特性为:
- 双端:带有prev和next指针,可以获取前置指针和后置指针
- 无环:表头节点的prev和表尾节点的next指针都指向null
- 具有计数器:链表维护了一个计数器len属性
- 多态性:使用void类型,表示任意类型
所以Redis中的链表是具有表头和表尾指针的双向不循环链表
二、压缩链表(ziplist)
压缩列表是列表键和哈希键的底层实现之一,在Redis3.2之前
- 当一个列表键只包含少量的列表项的时候,Redis就会使用压缩列表来做列表键的底层实现
- 当一个哈希键只包含少量的键值对,并且键值对的值要么是小整数型或者长度比较短的时候,就会使用压缩列表
压缩列表顾名思义,就是将列表进行压缩,那么主要目的就是节省空间。我们都知道无论是链表还是数组,对于数组来说,要先预留一部分空间,并且每一个节点的空间不能确定,对于链表来说不需要预留空间,但是每一个节点的空间还是不能确定,这就有可能造成一定的浪费。
将每一个节点的空间固定化主要是为了能够比较好的计算出下一个节点的位置,而Redis中的压缩列表是由一系列特殊编码的连续内存块组成的顺序性数据结构,压缩列表通过记住各个部分的类型、长度来做到确定下一个节点的位置以及压缩空间
下面是一个压缩列表的构成
每一个压缩列表节点entry,由以下三部分构成
- previous_entry_length:保存前一个节点长度,规则是如果前一个节点长度小于254字节,然后这个属性的长度就为1个字节,如果前一个节点长度大于254字节,这个属性的长度就为5字节
- encoding:记录节点的conent属性所保存数据的类型以及长度
- content:保存节点的值
连锁更新现象
连锁更新现象就是因为每个节点的previous_entry_length属性记录每一个节点的规则导致的,例如:我现在有一个压缩列表,每个节点的大小都为250-253字节之间,现在要插入一个新的节点,但是这个节点的大小为大于254,那么就造成以下情况,以这个节点之后的所有节点的previous_entry_length都要进行改变,因为插入的节点大于254,那么下一个节点的previous_entry_length属性就是5字节,那么下下个节点也要的previous_entry_length也要改为5字节
然后考虑极端情况,如果是插入的节点是在头节点,那么一整个压缩列表的previous_entry_length属性都要进行修改,所以每次进行操作的时间复杂度为O(n2)[n次重新分配,每一个重新分配为N]
但是造成这种连锁更新的现象的概率是很低的
三、快速列表
在Redis3.2之后,使用quicklist作为list列表的底层数据结构
快速列表结合了双向链表和压缩列表的优点,分别解决他们的缺点:
- 双向链表在插入节点上复杂度很低,但他的内存开销大,每个节点的地址不连续,容易参数内存碎片
- 压缩列表存储在一段连续的内存上,存储效率很高,但是不利于修改操作,插入和删除数据很麻烦,复杂度高
杂度很低,但他的内存开销大,每个节点的地址不连续,容易参数内存碎片
- 压缩列表存储在一段连续的内存上,存储效率很高,但是不利于修改操作,插入和删除数据很麻烦,复杂度高
而快速列表综合了双向链表和压缩列表的优点设计了quicklist这个结构