【最完整系列】Redis-结构篇-快速列表

注意:本系列文章分析的 Redis 源码版本:github.com/Sidfate/red…,是文章发布时间的最新版。node

前景提要

在介绍快速列表以前,建议你要先了解下 ziplist 和 adlist,特别是 ziplist (参考个人文章《【最完整系列】Redis-结构篇-压缩列表》),关于 adlist 下面我会简单解释一下。git

adlist

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

quicklist

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 的优缺点:

  • 双向链表便于在表的两端进行 push 和 pop 操做,可是它的内存开销比较大。首先,它在每一个节点上除了要保存数据以外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片,不利于内存管理。
  • ziplist 因为是一整块连续内存,因此存储效率很高。可是,它不利于修改操做,每次数据变更都会引起一次内存的 realloc。特别是当 ziplist 长度很长的时候,一次 realloc 可能会致使大批量的数据拷贝,进一步下降性能。

可是问题仍是类了,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 参数的限制。按照正值和负值有两种状况:

  • 当这个参数取正值的时候,就是刚好表示一个 quicklistNode 结构中 zl 所指向的 ziplist 所包含的数据项的最大值。list-max-ziplist-size 参数是由quicklist结构的 fill 字段来存储的,而 fill 字段是 16bit,因此它所能表达的值可以用 16bit 来表示。
  • 当这个参数取负值的时候,可以表示的 ziplist 最大长度是 64 Kb。而 ziplist 中每个数据项,最少须要 2 个字节来表示(详见《【最完整系列】Redis-结构篇-压缩列表》)。因此,ziplist中数据项的个数不会超过 32 Kb,用 16bit 来表达足够了。
相关文章
相关标签/搜索