Redis 底层使用了 ziplist、skiplist 和 quicklist 三种 list 结构来实现相关对象。顾名思义,ziplist 更节省空间、skiplist 则注重查找效率,quicklist 则对空间和时间进行折中。node
在典型的双向链表中,咱们有称为节点的结构,它表示列表中的每一个值。每一个节点都有三个属性:指向列表中的前一个和下一个节点的指针,以及指向节点中字符串的指针。而每一个值字符串值实际上存储为三个部分:一个表示长度的整数、一个表示剩余空闲字节数的整数以及字符串自己后跟一个空字符。redis
能够看到,链表中的每一项都占用独立的一块内存,各项之间用地址指针(或引用)链接起来。这种方式会带来大量的内存碎片,并且地址指针也会占用额外的内存。这就是普通链表的内存浪费问题。算法
此外,在普通链表中执行随机查找操做时,它的时间复杂度为 O(n),这对于注重效率的 Redis 而言也是不可接受的。这是普通链表的查找效率过低问题。数组
针对上述两个问题,Redis 设计了ziplist(压缩列表)、skiplist(跳跃表)和快速链表进行相关优化。数据结构
对于 ziplist,它要解决的就是内存浪费的问题。也就是说,它的设计目标就是是为了节省空间,提升存储效率。性能
基于此,Redis 对其进行了特殊设计,使其成为一个通过特殊编码的双向链表。将表中每一项存放在先后连续的地址空间内,一个 ziplist 总体占用一大块内存。它是一个表(list),但其实不是一个链表(linked list)。优化
除此以前,ziplist 为了在细节上节省内存,对于值的存储采用了变长的编码方式,大概意思是说,对于大的整数,就多用一些字节来存储,而对于小的整数,就少用一些字节来存储。ui
也正是为了这种高效率的存储,ziplist 有不少 bit 级别的操做,使得代码变得较为晦涩难懂。不过没关系,咱们本节的目标之一是为了了解 ziplist 对比普通链表,作了哪些优化,能够更好的节省空间。this
接下来咱们来正式认识下压缩列表的结构。编码
一个压缩列表能够包含任意多个节点(entry),每一个节点能够保存一个字节数组或者一个整数值。
图 1-1 展现了压缩列表的各个组成部分:
相关字段说明以下:
图 1-2 展现了一个包含五个节点的压缩列表:
节点的结构源码以下(ziplist.c):
typedef struct zlentry { unsigned int prevrawlensize, prevrawlen; unsigned int lensize, len; unsigned int headersize; unsigned char encoding; unsigned char *p; } zlentry;
如图 1-3,展现了压缩列表节点的结构。
回到咱们最开始对普通链表的认识,普通链表中,每一个节点包:
以图 1-4 为例:
图 1-4 展现了一个普通链表的三个节点,这三个节点中,每一个节点实际存储内容只有 1 字节,可是它们除了实际存储内容外,还都要有:
这样来看,存储 3 个字节的数据,至少须要 21 字节的开销。能够看到,这样的存储效率是很低的。
另外一方面,普通链表经过先后指针来关联节点,地址不连续,多节点时容易产生内存碎片,下降了内存的使用率。
最后,普通链表对存储单位的操做粒度是 byte,这种方式在存储较小整数或字符串时,每一个字节实际上会有很大的空间是浪费的。就像上面三个节点中,用来存储剩余空闲字节数的整数,实际存储空间只须要 1 bit,可是有了 1 byte 来表示剩余空间大小,这一个 byte 中,剩余 7 个 bit 就被浪费了。
那么,Redis 是如何使用 ziplist 来改造普通链表的呢?经过如下两方面:
一方面,ziplist 使用一整块连续内存,避免产生内存碎片,提升了内存的使用率。
另外一方面,ziplist 将存储单位的操做粒度从 byte 下降到 bit,有效的解决了存储较小数据时,单个字节中浪费 bit 的问题。
skiplist 是一种有序数据结构,它经过在每一个节点中维持多个指向其余节点的指针,来达到快速访问节点的目的。
skiplist 本质上是一种查找结构,用于解决算法中的查找问题。即根据指定的值,快速找到其所在的位置。
此外,咱们知道,"查找" 问题的解决方法通常分为两大类:平衡树和哈希表。有趣的是,skiplist 这种查找结构,由于其特殊性,并不在上述两大类中。但在大部分状况下,它的效率能够喝平衡树想媲美,并且跳跃表的实现要更为简单,因此有很多程序都使用跳跃表来代替平衡树。
本节没有介绍跳跃表的定义及其原理,有兴趣的童鞋能够参考这里。
认识了跳跃表是什么,以及作什么的,接下来,咱们再来看下在 redis 中,是怎么实现跳跃表的。
在 server.h
中能够找到跳跃表的源码,以下:
typedef struct zskiplist { struct zskiplistNode *header, *tail; unsigned long length; int level; } zskiplist; typedef struct zskiplistNode { robj *obj; double score; struct zskiplistNode *backward; struct zskiplistLevel { struct zskiplistNode *forward; unsigned int span; } level[]; } zskiplistNode;
Redis 中的 skiplist 和普通的 skiplist 相比,在结构上并无太大不一样,只是在一些细节上有如下差别:
对于 quicklist,在 quicklist.c
中有如下说明:
A doubly linked list of ziplists
它是一个双向链表,而且是一个由 ziplist 组成的双向链表。
相关源码结构可在 quicklist.h
中查找,以下:
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist. * We use bit fields keep the quicklistNode at 32 bytes. * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k). * encoding: 2 bits, RAW=1, LZF=2. * container: 2 bits, NONE=1, ZIPLIST=2. * recompress: 1 bit, bool, true if node is temporarry decompressed for usage. * attempted_compress: 1 bit, boolean, used for verifying during testing. * extra: 12 bits, free for future use; pads out the remainder of 32 bits */ typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *zl; unsigned int sz; /* ziplist size in bytes */ unsigned int count : 16; /* count of items in ziplist */ unsigned int encoding : 2; /* RAW==1 or LZF==2 */ unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */ unsigned int recompress : 1; /* was this node previous compressed? */ unsigned int attempted_compress : 1; /* node can't compress; too small */ unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode; /* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'. * 'sz' is byte length of 'compressed' field. * 'compressed' is LZF data with total (compressed) length 'sz' * NOTE: uncompressed length is stored in quicklistNode->sz. * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */ typedef struct quicklistLZF { unsigned int sz; /* LZF size in bytes*/ char compressed[]; } quicklistLZF; /* quicklist is a 32 byte struct (on 64-bit systems) describing a quicklist. * 'count' is the number of total entries. * 'len' is the number of quicklist nodes. * 'compress' is: -1 if compression disabled, otherwise it's the number * of quicklistNodes to leave uncompressed at ends of quicklist. * 'fill' is the user-requested (or default) fill factor. */ typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; /* total count of all entries in all ziplists */ unsigned int len; /* number of quicklistNodes */ int fill : 16; /* fill factor for individual nodes */ unsigned int compress : 16; /* depth of end nodes not to compress;0=off */ } quicklist;
上面介绍链表的时候有说过,链表由多个节点组成。而对于 quicklist 而言,它的每个节点都是一个 ziplist。quicklist 这样设计,其实就是咱们篇头所说的,是一个空间和时间的折中。
ziplist 相比普通链表,主要优化了两个点:下降内存开销和减小内存碎片。正所谓,事物老是有两面性。ziplist 经过连续内存解决了普通链表的内存碎片问题,但与此同时,也带来了新的问题:不利于修改操做。
因为 ziplist 是一整块连续内存,因此每次数据变更都会引起一次内存的重分配。当在 ziplist 很大的时候,每次重分配都会出现大批量的数据拷贝操做,下降性能。
因而,结合了双向链表和 ziplist 的优势,就有了 quicklist。
quicklist 的基本思想就是,给每个节点的 ziplist 分配合适的大小,避免出现因数据拷贝,下降性能的问题。这又是一个须要找平衡点的难题。咱们先从存储效率上分析:
可见,一个 quicklist 节点上的 ziplist 须要保持一个合理的长度。这里的合理取决于实际应用场景。基于此,Redis 提供了一个配置参数,让使用者能够根据状况,本身调整:
list-max-ziplist-size -2
这个参数能够取正值,也能够取负值。
当取正值的时候,表示按照数据项个数来限定每一个 quicklist 节点上 ziplist 的长度。好比配置为 2 时,就表示 quicklist 的每一个节点上的 ziplist 最多包含 2 个数据项。
当取负值的时候,表示按照占用字节数来限定每一个 quicklist 节点上 ziplist 的长度。此时,它的取值范围是 [-1, -5],每一个值对应不一样含义: