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号