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