Redis中list的底层原理
Redis中list的底层原理
一、概述
list 是一个有序的字符串列表,它按照插入顺序排序,并且支持在两端插入或删除元素。一个 list 类型的键最多可以存储 2^32 - 1 个元素。
redis3.2以后,list 类型的底层实现只有一种结构,就是quicklist。版本不同时,底层实现是不同的,下面会讲解。
二、应用场景
list 类型的应用场景主要是实现队列和栈,比如:
- 消息队列,利用 lpush 和 rpop 命令实现生产者消费者模式。
- 最新消息,利用 lpush 和 ltrim 命令实现固定长度的时间线。
- 历史记录,利用 lpush 和 lrange 命令实现浏览记录或者搜索记录。
三、底层原理
在讲解list结构之前,需要先说明一下list结构编码的更替,如下
在Redis3.2之前,list使用的是linkedlist(双向链表)和ziplist(压缩列表)
在Redis3.2~Redis7.0之间,list使用的是quickList(快速链表),是linkedlist和ziplist的结合
在Redis7.0之后,list使用的也是quickList,只不过将ziplist转为listpack,它是listpack、linkedlist结合版
3.1 linkedlist与ziplist
在Redis3.2之前,linkedlist和ziplist两种编码可以选择切换,它们之间的转换关系如图

3.2 ziplist结构
压缩列表(ziplist)是哈希键的底层实现之一。它是经过特殊编码的双向链表,和整数集合(intset)一样,是为了提高内存的存储效率而设计的。当保存的对象是小整数值,或者是长度较短的字符串,那么redis就会使用压缩列表来作为哈希键的实现。
ziplist的结构
struct ziplist<T> {
int32 zlbytes; // 整个压缩列表占用字节数
int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
int16 zllength; // 元素个数
T[] entries; // 元素内容列表,挨个挨个紧凑存储
int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}
-
zlbytes: 4byte 记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。
-
zltail:4byte 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节: 通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址。
-
zllength:2byte 记录了压缩列表包含的节点数量: 当这个属性的值小于 UINT16_MAX(65535)时, 这个属性的值就是压缩列表包含节点的数量; 当这个值等于UINT16_MAX 时,节点的真实数量需要遍历整个压缩列表才能计算得出。
-
entry X(entry[1-n]):压缩列表包含的各个节点,节点的长度由节点保存的内容决定。包含属性如下:
- prerawlen:记录前一个节点所占内存的字节数,方便查找上一个元素地址
- len:data根据len的首个byte选用不同的数据类型来存储data
- data:本元素的信息
-
zlend: 尾节点 恒等于255
ziplist转为linkedlist的条件可在redis.conf配置
list-max-ziplist-entries 512
list-max-ziplist-value 64
3.3 quickList(ziplist、linkedlist结合版)
quickList就是一个标准的双向链表的配置,有head 有tail;每一个节点是一个quicklistNode,包含prev和next指针。而每一个quicklistNode 包含 一个ziplist,*zp 压缩链表里存储键值。所以quicklist是对ziplist进行一次封装,使用小块的ziplist来既保证了少使用内存,也保证了性能。
quicklist存储了一个双向列表,每个列表的节点是一个ziplist,所以实际上quicklist并不是一个新的数据结构,它就是linkedlist和ziplist的结合,然后被命名为快速列表。


ziplist内部entry个数可在redis.conf配置
list-max-ziplist-size -2
# -5: 每个ziplist最多为 64 kb <-- 影响正常负载,不推荐
# -4: 每个ziplist最多为 32 Kb <-- 不推荐
# -3: 每个ziplist最多为 16 Kb <-- 最好不要使用
# -2: 每个ziplist最多为 8 Kb <-- 好
# -1: 每个ziplist最多为 4 Kb <-- 好
# 正数为ziplist内部entry个数
ziplist通过特定的LZF压缩算法来将节点进行压缩存储,从而更进一步的节省空间,而很多场景都是两端元素访问率最高,我们可以通过配置list-compress-depth来排除首尾两端不压缩的entry个数。
list-compress-depth 0
# - 0:不压缩(默认值)
# - 1:首尾第 1 个元素不压缩
# - 2:首位前 2 个元素不压缩
# - 3:首尾前 3 个元素不压缩
# - 以此类推
quickList(listpack、linkedlist结合版)
和Hash结构一样,因为ziplist有连锁更新问题,redis7.0将ziplist替换为listpack,下面是新quickList的结构图

quicklist表头结构
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* 所有列表包中所有条目的总数,占用16 bits,最大65536 */
unsigned long len; /* quicklistNode 的数量 */
signed int fill : QL_FILL_BITS; /* 单个节点的填充因子 */
unsigned int compress : QL_COMP_BITS; /* 不压缩的端节点深度;0=off */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
typedef struct quicklist {
//指向头部(最左边)quicklist节点的指针
quicklistNode *head;
//指向尾部(最右边)quicklist节点的指针
quicklistNode *tail;
//ziplist中的entry节点计数器
unsigned long count; /* total count of all entries in all ziplists */
//quicklist的quicklistNode节点计数器
unsigned int len; /* number of quicklistNodes */
//保存ziplist的大小,配置文件设定,占16bits
int fill : 16; /* fill factor for individual nodes */
//保存压缩程度值,配置文件设定,占16bits,0表示不压缩
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
| 说明 | |
|---|---|
| “head” | 表示快速链表的头部节点 |
| “tail” | 表示快速链表的尾部节点 |
| “count” | 表示快速链表中所有节点中元素的总数 |
| “len” | 表示快速链表中节点的个数 |
| “fill” | ziplist 节点的最大大小,值默认 8kb,大小超出后会新建一个 Ziplist,对应 list-max-ziplist-size 参数,占 16bit。 |
| “compress” | 节点压缩深度,表示节点是否使用 LZF 算法压缩,对应 list-compress-depth 参数,占1 6bit |
对于 “fill” 当数字为负数:
- -1:每个 ZipList 节点大小不能超过 4kb(建议)
- -2:每个 ZipList 节点大小不能超过 8kb(默认配置)
- -3:每个 ZipList 节点大小不能超过 16kb(一般不建议)
- -4:每个 ZipList 节点大小不能超过 32kb(不建议)
- -5:每个 ZipList 节点大小不能超过 64kb(正常工作量不建议)
对于 “fill” 当数字为正数:ZipList 节点最多包含的元素个数,最大值为 215215
对于 “compress” 节点,数字含义如下:
- 0:不压缩(默认)
- 1:QucikList 列表的两端各有1个ziplist节点不压缩,中间的节点压缩
- 2:QucikList 列表的两端各有2个ziplist节点不压缩,中间的节点压缩
- 3:QucikList 列表的两端各有3个ziplist节点不压缩,中间的节点压缩
- 以此类推,最大为 216216
quicklist节点结构
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *entry;
size_t sz; /* 当前entry占用字节 */
unsigned int count : 16; /* listpack元素个数,最大65535 */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* PLAIN==1 or PACKED==2 */
unsigned int recompress : 1; /* 当前listpack是否需要再次压缩 */
unsigned int attempted_compress : 1; /* 测试用 */
unsigned int extra : 10; /* 备用 */
} quicklistNode;
typedef struct quicklistNode {
struct quicklistNode *prev; //前驱节点指针
struct quicklistNode *next; //后继节点指针
//不设置压缩数据参数recompress时指向一个ziplist结构
//设置压缩数据参数recompress指向quicklistLZF结构
unsigned char *zl;
//压缩列表ziplist的总长度
unsigned int sz; /* ziplist size in bytes */
//ziplist中包的节点数,占16 bits长度
unsigned int count : 16; /* count of items in ziplist */
//表示是否采用了LZF压缩算法压缩quicklist节点,1表示压缩过,2表示没压缩,占2 bits长度
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
//表示一个quicklistNode节点是否采用ziplist结构保存数据,2表示压缩了,1表示没压缩,默认是2,占2bits长度
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
//标记quicklist节点的ziplist之前是否被解压缩过,占1bit长度
//如果recompress为1,则等待被再次压缩
unsigned int recompress : 1; /* was this node previous compressed? */
//测试时使用
unsigned int attempted_compress : 1; /* node can't compress; too small */
//额外扩展位,占10bits长度
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
listpack内部entry个数可在redis.conf配置
List-Max-listpack-size -2
# -5: 每个listpack最多为 64 kb <-- 影响正常负载,不推荐
# -4: 每个listpack最多为 32 Kb <-- 不推荐
# -3: 每个listpack最多为 16 Kb <-- 最好不要使用
# -2: 每个listpack最多为 8 Kb <-- 好
# -1: 每个listpack最多为 4 Kb <-- 好
# 正数为listpack内部entry个数
四、redis链表特性
- 双端:链表节点带有prev和next指针, 获取某个节点的前置节点和后置节点的复杂度都是O(1) 。
- 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
- 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1) 。
- 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。
总结
Redis list类型是一种可以存储一个有序的字符串列表的数据结构,它有以下特点:
- 可以在列表的两端进行快速的插入和删除操作,支持阻塞和非阻塞的读取方式
- 可以根据索引或范围来获取列表中的元素,支持反向遍历和排序等操作
- 可以用作栈、队列、消息队列等场景

浙公网安备 33010602011771号