注意:本系列文章分析的 Redis 源码版本:github.com/Sidfate/red…,是文章发布时间的最新版。node
在介绍快速列表以前,建议你要先了解下 ziplist 和 adlist,特别是 ziplist (参考个人文章《【最完整系列】Redis-结构篇-压缩列表》),关于 adlist 下面我会简单解释一下。git
adlist 其实就是一个常规的双向链表实现,show you source code:github
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
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
ok,在咱们切入正题前,先讲下在早期的 redis 版本中,列表键的实现分 2 种,元素数量少时用 ziplist,多时用 adlist,但在 3.2(网上资料查到的,不必定准确)以后的版本里,都用 quicklist 取代:算法
> lpush test_list 1
(integer) 1
> object encoding test_list
"quicklist"
复制代码
为何要专门设计一个 quicklist 来从新定义呢?接下来从源码的角度来解读。首先仍是看注释(redis 源码的注释真香):shell
quicklist.c - A doubly linked list of ziplists数据结构
而后再看下源码结构实现:post
typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
unsigned int sz; /* ziplist 占用的字节总数 */
unsigned int count : 16; /* ziplist 的元素个数 */
unsigned int encoding : 2; /* 是否被压缩,2表示被压缩,1表示原生 */
unsigned int container : 2;
unsigned int recompress : 1;
unsigned int attempted_compress : 1;
unsigned int extra : 10;
} quicklistNode;
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* 全部 ziplists 的元素总和 */
unsigned long len; /* quicklistNodes 的个数 */
int fill : 16;
unsigned int compress : 16;
} quicklist;
复制代码
字段的详细解释请参照本文末尾的字段详解部分。性能
所你们明白了吗,为何我在一开始让你们先去了解下 ziplist 和 adlist,由于 quicklist 其实就是一个以 ziplist 为节点(quicklistNode 中存放指向 ziplist 的指针)的 adlist,没图说个JB:单元测试
知道结构后再回到咱们最初的问题,quicklist 的结构为何这样设计呢?quicklist 平衡了 ziplist 和 adlist 的优缺点:
可是问题仍是类了,quicklist 中 quicklistNode 包含多长的 ziplist 多少合适呢?长度若是小了,跟普通的双向链表也就差很少了,仍是有内存碎片的问题;长度大了,每一个 quicklist 节点上的 ziplist 须要大片的连续内存,操做内存的效率仍是降低了。因此这个长度确定是一个平衡值,它是 redis 提供的一个选项配置,默认是 -2,来看下官方说明:
# -5: max size: 64 Kb <-- not recommended for normal workloads
# -4: max size: 32 Kb <-- not recommended
# -3: max size: 16 Kb <-- probably not recommended
# -2: max size: 8 Kb <-- good
# -1: max size: 4 Kb <-- good
# Positive numbers mean store up to _exactly_ that number of elements
# per list node.
# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
# but if your use case is unique, adjust the settings as necessary.
list-max-ziplist-size -2
复制代码
这里我就不翻译了,原生英文解释的很清楚了,惟一一点要说明下的是,当 list-max-ziplist-size 设置为正数时,表示每一个 list 节点中储存元素个数。
你们仔细看 quicklist 的源码结构时,可能还注意到出现了不少 compress 的字样,这是由于 redis 为 quicklist 提供了一套压缩机制。
当 quicklist 很长的时候,最容易被访问的极可能是两端的数据,中间的数据被访问的频率比较低(访问起来性能也很低)。若是应用场景符合这个特色,redis 还提供了一个选项,可以把中间的数据节点进行压缩,从而进一步节省内存空间。Redis的配置参数 list-compress-depth
就是用来完成这个设置的。
# 列表也能够被压缩。
# 压缩深度指的是列表两侧开始不须要 ziplist 节点的深度(下面会解释)。
# 为了执行快速的 push/pop 操做,列表的头和尾一般不压缩。
# 设置以下:
# 0: 禁用压缩机制
# 1: 压缩深度 1 表示压缩除了头和尾以外的全部内部节点。例如结构:
# [head]->node->node->...->node->[tail]
# 由于[head], [tail]永远不会被压缩,它们直接的 node 都后被压缩。
# 2: [head]->[next]->node->node->...->node->[prev]->[tail]
# 2 表示不压缩 head,head->next,tail->prev 和 tail, 它们以前的 node 都压缩。
# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
# 以此类推...
list-compress-depth 0
复制代码
这个参数默认是 0 也就是不压缩,Redis对于 quicklist 内部节点的压缩算法,采用的 LZF —— 一种无损压缩算法,有兴趣的能够看下 zh.wikipedia.org/wiki/LZFSE。
以前的内容已经对源码结构中大多数的字段作了说明,可是还遗留一些字段我在这里统一解释下。
首先补充一个结构 quicklistLZF,后面的说明中会出现:
typedef struct quicklistLZF {
unsigned int sz; /* LZF size in bytes*/
char compressed[];
} quicklistLZF;
复制代码
属性 | 大小 | 含义 |
---|---|---|
prev | 8字节 | 指向链表前一个节点的指针。 |
next | 8字节 | 指向链表后一个节点的指针。 |
zl | 8字节 | 数据指针。若是当前节点的数据没有压缩,那么它指向一个 ziplist 结构;不然,它指向一个 quicklistLZF 结构。 |
sz | 4字节 | ziplist 占用的字节总数。若是指向的 ziplist 被压缩,仍然表示压缩前的字节总数。 |
count | 16位 | ziplist 包含的元素个数。 |
encoding | 2位 | 表示ziplist是否压缩了。目前只有两种取值:2表示被压缩了(并且用的是LZF压缩算法),1表示没有压缩。 |
container | 2位 | 数据容器。为1时表示 NONE,即一个 quicklist 节点下面直接存数据,为2时表示ZIPLIST,即便用ziplist存数据。 |
recompress | 1位 | bool值,当咱们使用相似lindex这样的命令查看了某一项原本压缩的数据时,须要把数据暂时解压,这时就设置 recompress = 1 作一个标记,等有机会再把数据从新压缩。 |
attempted_compress | 1位 | bool值,在单元测试的时候用到。 |
extra | 10位 | 扩展字段,以备后用。 |
须要注意的是,我发现网上的不少文章都没有提到的一点,官方给出的解释中说明了 quicklistNode 是一个 32 字节的结构,这应该是针对 64 位系统而言的,由于 prev,next 和 zl 都是指针,在 64 位系统中占 8 字节,如下的结构同理。
属性 | 大小 | 含义 |
---|---|---|
head | 8字节 | 指向头节点的指针。 |
tail | 8字节 | 指向尾节点的指针。 |
count | 8字节 | 全部 ziplists 的元素总个数。 |
len | 8字节 | quicklist 节点的个数。 |
fill | 16位 | ziplist大小设置,存放 list-max-ziplist-size 参数的值。 |
compress | 16位 | 节点压缩深度,存放 list-compress-depth 参数的值。 |
另外须要注意一点的是 quicklist 结构在 64 位系统中是占 40 个字节,可是如上计算我得出的长度是 36 字节,这里面涉及到告终构体字节对齐约定,目的的话仍是为了提高数据的读取效率。
属性 | 大小 | 含义 |
---|---|---|
sz | 4字节 | 压缩后的ziplist大小 |
compressed | 待定 | LZF 压缩后的数据 |
为何 quicklistNode 中的 count 用 16 位就能够表示?
咱们已经知道,ziplist 大小受到 list-max-ziplist-size
参数的限制。按照正值和负值有两种状况:
list-max-ziplist-size
参数是由quicklist结构的 fill 字段来存储的,而 fill 字段是 16bit,因此它所能表达的值可以用 16bit 来表示。