Redis针对每种数据类型(type) 可以采用至少两种编码方式来实现。如下如图
注意3.2版本之后的redis list类型的数据内部都是通过quicklist实现的。
了解编码和类型对应关系之后, 我们不禁疑惑Redis为什么对一种数据结构实现多种编码方式?
主要原因是Redis作者想通过不同编码实现效率和空间的平衡。 比如当我们的存储只有10个元素的列表, 当使用双向链表数据结构时, 必然需要维护大量的内部字段如每个元素需要: 前置指针, 后置指针, 数据指针等, 造成空间浪费, 如果采用连续内存结构的压缩列表(ziplist) , 将会节省大量
内存, 而由于数据长度较小, 存取操作时间复杂度即使为O(n2) 性能也可满足需求。
编码类型转换在Redis写入数据时自动完成, 这个转换过程是不可逆的, 转换规则只能从小内存编码向大内存编码转换。其中Redis之所以不支持编码回退, 主要是数据增删频繁时, 数据向压缩编码转换非常消耗CPU, 得不偿失。 以上示例用到了list-max-ziplist-entries参数, 这个参数用来决定列表长度在多少范围内使用ziplist编码。 当然还有其他参数控制各种数据类型的编码,如下图。
掌握编码转换机制, 对我们通过编码来优化内存使用非常有帮助。 下面以hash类型为例, 介绍编码转换的运行流程。
理解编码转换流程和相关配置之后, 可以使用config set命令设置编码相关参数来满足使用压缩编码的条件。 对于已经采用非压缩编码类型的数据如hashtable、 linkedlist等, 设置参数后即使数据满足压缩编码条件, Redis也不会做转换, 需要重启Redis重新加载数据才能完成转换。
下面将介绍着6中内部数据结构(sds,ziplist,hashtable,intset,skiplist,linkedlist)
1.sds
Redis没有采用原生C语言的字符串类型而是自己实现了字符串结构, 内部简单动态字符串(simple dynamic string, SDS)
Redis自身实现的字符串结构有如下特点:
·O(1) 时间复杂度获取: 字符串长度、 已用长度、 未用长度。
·可用于保存字节数组, 支持安全的二进制数据存储。
·内部实现空间预分配机制, 降低内存再分配次数。
·惰性删除机制, 字符串缩减后的空间不释放, 作为预分配空间保留。
字符串对象的编码可以是 INT、RAW 或 EMBSTR,对于8个字节以下的int类型,采用INT方式,当字符串值得长度大于44字节使用RAW,小于等于44字节使用EMBSTR。
Redis在3.0引入EMBSTR编码,这是一种专门用于保存短字符串的一种优化编码方式,这种编码和RAW编码都是用sdshdr简单动态字符串结构来表示。RAW编码会调用两次内存分配函数来分别创建redisObject和sdshdr结构,而EMBSTR只调用一次内存分配函数来分配一块连续的空间保存数据,比起RAW编码的字符串更能节省内存,以及能提升获取数据的速度。
不过要注意!EMBSTR是不可修改的,当对EMBSTR编码的字符串执行任何修改命令,总会先将其转换成RAW编码再进行修改;而INT编码在条件满足的情况下也会被转换成RAW编码。
2.ziplist
ziplist编码主要目的是为了节约内存, 因此所有数据都是采用线性连续的内存结构。 ziplist编码是应用范围最广的一种, 可以分别作为hash、 list、zset类型的底层数据结构实现。 首先从ziplist编码结构开始分析, 它的内部结构类似这样: <…>
。 一个ziplist可以包含多个entry(元素) , 每个entry保存具体的数据(整数或者字节数组)。
ziplist结构字段含义:
1) zlbytes: 记录整个压缩列表所占字节长度, 方便重新调整ziplist空间。 类型是int-32, 长度为4字节。
2) zltail: 记录距离尾节点的偏移量, 方便尾节点弹出操作。 类型是int-32, 长度为4字节。
3) zllen: 记录压缩链表节点数量, 当长度超过216-2时需要遍历整个列表获取长度, 一般很少见。 类型是int-16, 长度为2字节。
4) entry: 记录具体的节点, 长度根据实际存储的数据而定。
a) prev_entry_bytes_length: 记录前一个节点所占空间, 用于快速定位上一个节点, 可实现列表反向迭代。
b) encoding: 标示当前节点编码和长度, 前两位表示编码类型: 字符串/整数, 其余位表示数据长度。
c) contents: 保存节点的值, 针对实际数据长度做内存占用优化。
5) zlend: 记录列表结尾, 占用一个字节。
根据以上对ziplist字段说明, 可以分析出该数据结构特点如下:
·内部表现为数据紧凑排列的一块连续内存数组。
·可以模拟双向链表结构, 以O(1) 时间复杂度入队和出队。
·新增删除操作涉及内存重新分配或释放, 加大了操作的复杂性。
·读写操作涉及复杂的指针移动, 最坏时间复杂度为O(n2) 。
·适合存储小对象和长度有限的数据。
3.hashtable
hashtable的实现和java的hashmap实现几乎一致,提供O(1)时间复杂度的查询和新增操作。对于redis的基础数据类型。ziplist提供O(N^2)的查找,新增操作(查找时遍历整个entry对于每个key都进行比较),而hashtable提供了o1。所以当少量的数据和小key时可以采用ziplist来提高空间利用率,小数据的稿时间复杂度操作并不会太影响性能。大量数据和的key需要用hashtable来提高速度,尽管hashtable空间利用率不高。
4.intset
intset结构由encoding、length 、contents 3个属性构成,而且内部的数据结构排列紧密,类似于ziplist,但是intset里面存的全都是顺序排列的整数。由于是顺序排列的整数,所以查找可用二分查找,时间复杂度o(logn)。因为是紧凑的数据结构,所以插入元素涉及到数据的复制,时间复杂度为
o(n)。因为需要有序来提供二分查找,所以需要内部元素都是数字。因为查找和新增,删除时间复杂度都不是o1,所以当存在大量数据是需要转成hashtable。set的两种内部实现和之间的转换关系由此可见。
5.quicklist
考虑到链表的附加空间相对太高,prev 和 next 指针就要占去 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。后续版本对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。下面看旧版本redis的list内部编码实现:
linkedlist
双向链表在表的两端进行push和pop操作十分的便节,但是它的内存开销比较大。
首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;
其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
ziplist
ziplist由于是一整块连续内存,所以存储效率很高。
首先,它不利于修改操作,每次数据变动都会引发一次内存的realloc。
其次,当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。
Redis基于空间和时间的考虑,于是quicklist结合双向链表和ziplist的优点。
可见 quicklist的实现是每个节点都是ziplist的linkedlist。并且ql持有head和tail的ziplist。方便头尾节点的push,pop(linkedlist的优点)。节点采用ziplist从而减少了每个节点元素头尾引用和内存空间碎片。每个ziplist长度的控制减少了ziplist中的新增删除操作的时间复杂度,所以,quicklist集合了ll和zl的优点。
list-max-ziplist-size
每个quicklist节点上的ziplist越短,则内存碎片越多。内存碎片多了,有可能在内存中产生很多无法被利用的小碎片,从而降低存储效率。这种情况的极端是每个quicklist节点上的ziplist只包含一个数据项,这就蜕化成一个普通的双向链表了。
每个quicklist节点上的ziplist越长,则为ziplist分配大块连续内存空间的难度就越大。有可能出现内存里有很多小块的空闲空间(它们加起来很多),但却找不到一块足够大的空闲空间分配给ziplist的情况。这同样会降低存储效率。这种情况的极端是整个quicklist只有一个节点,所有的数据项都分配在这仅有的一个节点的ziplist里面。这其实蜕化成一个ziplist了。
可见,一个quicklist节点上的ziplist要保持一个合理的长度。那到底多长合理呢?Redis提供了一个配置参数list-max-ziplist-size,就是为了让使用者可以来根据实际应用场景进行调整优化。
list-compress-depth
其表示一个quicklist两端不被压缩的节点个数。注:这里的节点个数是指quicklist双向链表的节点个数,而不是指ziplist里面的数据项个数。实际上,一个quicklist节点上的ziplist,如果被压缩,就是整体被压缩的。
当表list存储大量数据的时候,最容易被访问的很可能是两端的数据,中间的数据被访问的频率比较低(访问起来性能也很低)。如果应用场景符合这个特点,那么list还提供了一个选项,能够把中间的数据节点进行压缩,从而进一步节省内存空间。Redis的配置参数list-compress-depth就是用来完成这个设置的。
6.skiplist
SkipList是一种有序的数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均O(logN),最坏O(N)负责度的节点查找。还可以通过顺序性操作来批量处理节点,在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且跳跃表的实现比平衡树来的更为简单,所以有不少程序使用跳跃表来代替平衡树。
Redis中的有序集合键使用跳跃表作为底层的实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis会使用跳跃表来作为有序集合的底层实现。
skiplist与平衡树、哈希表的比较
skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
从算法实现难度上来比较,skiplist比平衡树要简单得多。
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
这两个参数是指 什么情况下一个zset的内部编码会从ziplist转为skiplist,可见ziplist几个操作都是高时间复杂度的操作,但是内存利用率高,而skiplist操作时间复杂度更小(需要更多的内存)。
Redis为什么用skiplist而不用平衡树?
在前面我们对于skiplist和平衡树、哈希表的比较中,其实已经不难看出Redis里使用skiplist而不用平衡树的原因了。现在我们看看,对于这个问题,Redis的作者 @antirez 是怎么说的:
There are a few reasons:
They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.
内存利用率比平衡数高
A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.
zset经常用于ZRANGE 和ZREVRANGE 操作,最坏情况下skiplist和平衡树的性能差不多好。
They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.
skiplist实现简单。
这里从内存占用、对范围查找的支持和实现难易程度这三方面总结的原因,我们在前面其实也都涉及到了。