1、前言
欢迎大家来到通俗易懂Redis,先从redis的基础数据结构说起,给大家简单的进行剖析。Redis有5种基础数据类型,分别为:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合)。Redis 底层的数据结构一共有6种,如下图第二排部分,它和数据类型对应关系也如下图:
今天主要给大家分享我对list数据类型的一点见解,有错误的地方请大家指正。
2、list(列表)介绍
Redis的列表相当于Java语言里面的LinkedList,注意它是链表而不是数组。这意味着list的插入和删除操作非常快,时间复杂度为 O(1)。通过上面的关系对应图,我们发现list对应的底层数据结构不只是链表一种,还有压缩列表。两种底层数据结构分别是在不同的场景下使用,一会我们详细说明。Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进Redis的列表,另一个线程从这个列表中轮询数据进行处理。
3、list(列表)基本使用
3.1、右边进左边出:队列
//rpush添加数据到列表
> rpush animals cat dog pig
(integer) 3
> llen animals
(integer) 3
//lpop取出列表左边第一个数据(移除并返回列表的头元素)
> lpop animals
"cat"
> lpop animals
"dog"
> lpop animals
"pig"
> lpop animals
(nil)
3.2、右边进右边出:栈
> rpush animals cat dog pig
(integer) 3
//rpop取出列表右边第一个数据(移除并返回列表的头元素)
> rpop animals
"cat"
> rpop animals
"dog"
> rpop animals
"pig"
> rpop animals
(nil)
3.3、获取指定区间的数据,列表的长度
> lrange animal 0 2
1) "cat"
2) "dog"
3) "pig"
> llen animal
(integer) 3
//index可以为负数,`index=-1`表示倒数第一个元素,同样`index=-2`表示倒数第二个元素。
//获取所有元素,O(n) 慢操作 慎用
> lrange books 0 -1
1) "cat"
2) "dog"
3) "pig"
4、list(列表)原理
list列表的底层数据结构分别用了双向链表和压缩列表,下面分别介绍一下。
4.1、redis双向列表
我们先看一下链表节点结构:
typedef struct listNode {
struct listNode *prev; //前置节点
struct listNode *next; //后置节点
void *value; //节点的值
} listNode;
上面代码中展示了一个最基本的双向链表,每一个listNode都包含它的前置节点、后置节点和当前节点的值三部分。Redis在双向链表的listNode结构体基础上又封装了一个list结构,这样操作起来会更方便,封装的list结构如下:
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封装的双向链表的优点:
1)list结构记录了了表头指针head和表尾节点tail,所以获取链表的表头节点和表尾节点的时间复杂度是O(1)
2)list结构提供len,用来表示链表节点数量,所以获取链表中的节点数量的时间复杂度也是O(1)
3)list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值
4)listNode 链表节点带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1)
既然redis封装了list结构,使得列表中的很多操作都非常的高效,又为什么提出了压缩列表(zipList)呢?原因是链表的缺陷也是比较明显的,我们知道对比数组,链表在内存空间上是不连续的,这样无法很好的利用CPU缓存,非连续的存储也不能很好的利用内存。为了高效的利用内存,节省空间,又设计出了非常有用的压缩列表。
4.2、redis压缩列表(ziplist)
Redis为了节约内存空间使用,list、zset和hash在元素个数较少的时候,采用压缩列表 (ziplist) 进行存储,压缩列表是一块连续的内存空间,元素之间紧挨着存储。说白了压缩列表是Redis为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。
4.2.1、压缩列表(ziplist)结构介绍
压缩列表的结构如下:
struct ziplist<T> {
int32 zlbytes; // 记录整个压缩列表占用对内存字节数;
int32 zltail_offset; // 记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量
int16 zllength; // 记录压缩列表包含的节点数量;
T[] entries; // 元素内容列表,挨个挨个紧凑存储
int8 zlend; // 标记压缩列表的结束点,特殊值 OxFF(十进制255)。
}
在内存上表现形式,我们可以理解为下图所示结构:
通过压缩列表的结构我们可以总结一下压缩列表的优势:
- 查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)
- 内存空间连续,可以节约空间,充分利用空间,最大的优势
压缩列表的优点是节省内存,缺点是插入元素的复杂度较高平均O(N)最坏O(N^2), 但是在小数据量的情况下,这种复杂度也是可以接受的。所以,压缩列表适合用在数据量比较少的时候,数据量大的时候,使用压缩列表就不太合适,一个是因为插入时间复杂度比较高,再就是可能会发生链锁更新。
4.2.2、连锁更新
通过结构图我们可以知道,压缩列表里的每个节点中都用prevlen属性都记录了前一个节点的长度,而且prevlen属性的空间大小跟前一个节点长度值有关,比如:
- 如果前一个节点的长度小于254字节,那么prevlen属性需要用1字节的空间来保存这个长度值;
- 如果前一个节点的长度大于等于254字节,那么prevlen属性需要用5 字节的空间来保存这个长度值。第一个字节是 0xFE(254),剩余四个字节表示字符串长度
因为prevlen是一个变长的整数,所以更新或者新增数据的时候就会产生连锁更新 现在假设有一个压缩列表,列表中每个节点的长度都是253字节,因为这些节点长度值小于 254 字节,所以prevlen属性需要用 1 字节的空间来保存这个长度值。如下图:
现在新增一个长度大于等于254字节的节点entry0加入到表头,因为entry1节点的prevlen属性只有1字节大小,无法保存新节点的长度,此时需要将entry1节点的prevlen属性从原来的1字节大小扩展为5字节大小。
entry1字节扩展长度后,它本身的长度也超过了254字节,所以导致entry2节点的prevlen属性也得从原来的1字节大小扩展为5字节大小。
说到这里,大家应该会意识到一件事,因为例子中每个节点的长度都很长,前置节点的改变,会引起后边节点的改变,就是扩展entry1引发了对entry2扩展一样,扩展entry2也会引发对entry3的扩展,而扩展entry3又会引发对entry4的扩展…. 一直持续到结尾。这种在特殊情况下产生的连续多次空间扩展操作就叫做连锁更新
试想一下,如果列表很长,发生连锁更新的话,占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能。因此,就像前面所说的压缩列表只会用于保存的节点数量不多的场景,只要节点数量足够小,即使发生连锁更新,也是能接受的。数据量大的话还是会使用双向链表来存储列表。
以上就是今天全部的内容了,主要目的是让大家了解redis的list列表基本数据类型,后续有新的知识点会继续补充!!!感谢大家!!!