文章目录
1. 概述
Redis的列表类似于Java语言当中的LinkedList,但是还是存在着很大的区别的。
Redis3.2版本的前,使用两种数据结构作为底层实现:
- 压缩列表zipList
- 双向链表LinkedList
双向链表占用的内存比压缩链表要多,所以当创建新的列表键的时候,会优先考虑使用压缩列表,并且在有需要的时候,会转换成双向链表。
Redis3.2版本开始,Redis修改了list的底层实现,将压缩列表和双向链表结合,称之为quickList。
下面就先从3.2版本的底层结构开始说明。
2. 压缩列表zipList
压缩列表设计的初衷就是为了节约内存。
它是由一系列特殊编码的内存块构成的,使用一块连续的内存空间存储。每个元素长度不同,采用的是变长编码。
如何起到节约内存的呢?
假设列表中的元素内容都很小,但是如果是双向列表的话就需要维护头尾两个指针,这是很浪费空间的。所以zipList在结构上可以得到上一个结点的长度和当前结点的长度。那么通过上一个结点的长度,就可以将指针定位到上一个元素起始的位置,而通过当前结点的长度,就可以将指针定位到下一个元素的起始位置。
zipList的内存内存结构如下图所示:
包括了头部信息和结点列表信息。
- zlbytes: 表示整个zipList占用的内存字节数,对zipList进行内存重分配,或者计算末端时使用。
- zltail: 表示到达ziplist表尾结点的偏移量。通过这个偏移量,可以直接定位到表位元素。
- zllen:表示zipList中结点的数量。当这个值小于65535时,这个值就是zipList中结点的数量,当等于65535的时候,需要遍历整个zipList才能计算出结点的数量。
- zlend:255的二进制值,用于表示zipList的末端
每个entry表示结点元素信息,采用的是变长编码。结点entry的结构如下:
typedef struct zlentry { // 压缩列表节点
unsigned int prevrawlensize, prevrawlen; // prevrawlen是前一个节点的长度,prevrawlensize是指prevrawlen的大小,有1字节和5字节两种
unsigned int lensize, len; // len为当前节点长度 lensize为编码len所需的字节大小
unsigned int headersize; // 当前节点的header大小
unsigned char encoding; // 节点的数据类型
unsigned char *p; // 指向节点的指针
} zlentry;
就和上面说的一样,每个结点可以得到前一个结点的长度 和当前结点的长度。
那么是如何变长编码的呢?
注意这里有一个prevrawlensize属性,它记录的是prevrawlen的大小,分成了两种
- 若前一个结点的长度小于254字节,那么则使用1字节来存储prevrawlen;
- 如果前一个结点的长度大于等于254字节,那么将第一个字节设置为254,然后接下来4个字节保存实际的长度。
上面的结构体的内容非常的多,上面的方式只是为了描述一个结点的设计需要考虑的东西,而实际上的entry结点的属性比上面要少一些,如下:前一个结点的长度就通过prevlen记录,而当前结点的长度就根据这3个属性的长度推出。
struct entry {
int<var> prevlen; # 前一个 entry 的字节长度
int<var> encoding; # 元素类型编码
optional byte[] content; # 元素内容
}
3.ziplist连锁更新问题
每个zlentry结点都存储着前一个结点的所占的字节数,而这个数值是采用变长编码的。假设存在一个压缩列表,其中包含了e1,e2,e3…,e1结点的大小小于254,则e2中采用的是第一种的编码方式,也就是prevlen为1字节,若在e1和e2之间插入一个新的结点,这个新的结点的大小超过了254.那么此时e2中记录前一个结点的编码方式就需要修改,会多出4个字节。那么e2的整体长度就发生了变化,就会引起e3.prevlen发生改变,以此类推,当存在大量的结点接近254的时候,就会发生严重的连锁更新问题。
4.双向链表LinkedList
当链表中entry结点的数量超过512个、或单个value 长度超过64字节,底层就会转化成linkedlist编码。linkedlist是标准的双向链表,Node节点包含prev和next指针,可以进行双向遍历。还保存了 head 和 tail 两个指针,因此,对链表的表头和表尾进行插入的复杂度都为 (1) —— 这是高效实现 LPUSH 、 RPOP、 RPOPLPUSH 等命令的关键。linkedlist结构比较简单。
5.quickList
3.2版本开始采用quickList作为list的底层实现,结合了ziplist和LinkedList的优点。
quickList是一个zipList组成的双向链表。每个结点使用zipList来保存数据。本质上说quickList就是由一个一个小的zipList串起来的链表。如下图所示:
每个quickListNode包含了prev和next指针,分别指向前一个和下一个的qucikListNode。
quickListNode中又包含了zipList。
6.List的相关指令
介绍常用的一些指令
6.1push
-
lpush
使用方法:lpush key value [value…]
将一个或者多个value插入到列表key的表头
当key不是列表类型或者key不存在 那么会返回一个错误
-
Lpushx
使用方法:lpushx key value [value…]
和上面指令的区别在于,仅当key存在并且是一个列表的时候,才会执行,将value 插入列表的表头
-
rpush/rpushx
和上面的区别在于是在列表的表尾插入
6.2pop
-
lpop
使用方法:lpop key
移除并返回列表的表头元素,key不存在的时候,返回nil
-
rpop
使用方法: rpop key
和上面的区别在于返回列表的表尾元素
6.3 rpoplpush
使用方法:rpoplpush source destination
原子操作,将列表source中的表尾元素弹出,返回给客户端,并将该元素插入到destination的表头
若source不存在则返回nil
6.4 lrem
使用方法:lrem key count value
根据count的值移除列表key中参数与value相等的元素
- 当count > 0:从表头开始向后,移除value相等的元素,数量为count
- 当count < 0:从表尾开始向前,移除value相等的元素,数量为count的绝对值
- 当count = 0:移除列表中所有与value相等的元素。
返回被移除元素的数量。
6.5 llen
使用方法:llen key
返回列表key的长度,key不存在返回0
6.6 lindex
使用方法:lindex key index
返回列表key中下标为index的元素
0表示第一个元素
index可以是负数,-1表示最后一个元素,-2表示倒数第二个元素,以此类推。
若key不是列表返回错误。
6.7 linsert
使用方法:linsert key before|after pivot value
将值value插入在列表key中 pivot的之前或之后的位置。
- 若pivot不存在于列表中,不执行任何操作。返回-1
- 若key不存在,不执行任何操作。返回0
- 若key不是列表,返回一个错误。
6.8 lset
使用方法:lset key index value
将列表key中下标为index的值设置为value
- 若index参数超过列表返回,或对一个空列表进行lset,返回一个错误。
- 操作成功返回ok
6.9 lrange
使用方法:lrange key start stop
返回列表当中 start到stop的元素,闭区间
- 若start下标比列表最大下标大,返回一个空列表
- 若stop下标比列表最大下标大,会将stop设置为最大下标的值
6.10 ltrim
使用方法:ltrim key start stop
对列表key 进行修剪,仅保留start到stop返回的数据,闭区间,其他删除。
- 若key不是列表返回错误
- 若start>stop或start下标比列表最大下标大,则返回一个空列表,表示全部清空
- 若stop下标比列表最大下标大,会自动将stop设置为最大下标的值