最近看了相关的Redis设计核心相关的书籍,对Redis有了一些小的认识,然后自己也做一些产出加深映象,我会从几个方面去总结Redis设计的核心内容:Redis底层数据结构总结、Redis高性能由哪些基础支撑、Redis应用场景、那些有趣的功能。
本篇主要内容是Redis底层数据结构总结。Redis供用户直接使用的数据结构有String、List、Set、Zset、Hash等结构,而这些结构下层又基于一些数据结构,这些数据结构被设计的非常优美,来提供了Redis高效的性能、低内存占用、多样的功能等,他们有Dict、RedisObject、SDS、ZipList、QuickList、Listpak、Skiplist,我将从是什么、为什么使用、设计思想、特点、实现结构几个方面去总结。续上次说到了Ziplist,本章将继续介绍剩余的底层数据结构。
目录
前言
概述
QuickList
是什么
为什么使用
设计思想
特点
实现结构
Listpak
是什么
为什么使用
设计思想
特点
实现结构
如何获取尾部元素偏移量?
Skiplist
是什么
为什么使用
设计思想
特点
为什么使用随机层数?
skiplist与平衡树、哈希表的比较:
Redis是如何在ziplist和skiplist中选择的:
实现结构
quicklist是List的底层数据结构,他是ziplist和linkedlist的混合体,将双向链表按段划分,每一段使用ziplist来紧凑存储,多个ziplist之间使用双向指针串联
所以quciklist采用了双方的优点,避免了双方的缺点,采用分段的方式将一个长链表划分成多段node节点 ,每一段node节点使用ziplist存储:1.采用ziplist节省内存的优点 2.采用ziplist连续内存,降低内存碎片率 3.避免生成大的ziplist减少内存空间重分配的影响,4.减少node节点数量,使维护的指针数量变少节省内存空间;5.提供数据压缩功能,因为一般链表操作首尾更多,对中间元素来提高存储效率
Quicklist采用了Ziplist高效存储的特点,并且避免了将ziplist分段存储,避免单个Ziplist过大导致的操作缓存,将Ziplist当成一个Node使用,多个Node串联形成一条双向链表。
图来源于
typedef struct quicklist {
quicklistNode *head; //指向头节点的指针
quicklistNode *tail; //指向尾节点的指针
unsigned long count; //总的元素数量
unsigned long len; //node的数量
int fill : 16; //ziplist中entry能保存的数量,由list-max-ziplist-size配置项
控制
unsigned int compress : 16; //压缩深度,由list-compress-depth配置项控制
}
typedef struct quicklistNode {
struct quicklistNode *prev; 8byte //前节点指针
struct quicklistNode *next; 8byte //后节点指针
unsigned char *zl; 8byte //数据指针。当前节点的数据没有压缩,那么它
指向一个ziplist结构;否则,它指向一个quicklistLZF结构。
unsigned int sz; //zl指向的ziplist实际占用内存大小。需要注意的是:如果
ziplist被压缩了,那么这个sz的值仍然是压缩前的ziplist大小。
unsigned int count : 16; //ziplist里面包含的数据项个数
unsigned int encoding : 2; //ziplist是否压缩。取值:1--ziplist,2--quicklistLZF
unsigned int container : 2; //存储类型,目前使用固定值2 表示使用ziplist存储
unsigned int recompress : 1; //当我们使用类似lindex这样的命令查看了某一项本来压缩的数
据时,需要把数据暂时解压,这时就设置recompress=1做一个标记,等有机会再把数据重新压缩
unsigned int attempted_compress : 1; //Redis自动化测试程序有用
unsigned int extra : 10; //它扩展字段,目前没有使用
}
typedef struct quicklistLZF {
unsigned int sz; //压缩后的ziplist大小
char compressed[]; //柔性数组,存放压缩后的ziplist字节数组
} quicklistLZF;
使用ziplist+linkedlist的实现,其中ziplist用来存储元素,而上层提供node节点来减少单个ziplist的数量,从而即高效,又占用内存少,内存碎片率低,并且quicklist还提供数据压缩功能。quicklist内部包含quicklistNode,quicklistNode使用ziplist存储。还可以使用quicklistLZF压缩元素。
更多内容请参考Redis专栏。
Redis 5.0 又引入了一个新的数据结构 listpack,它是对 ziplist 结构的改进,在存储空间上会更加节省,而且结构上也比 ziplist 要精简,占用内存更少。
Ziplist有以下缺点:
而listpack:
listpack结合了ziplist的优点:紧凑字节数组+压缩编码+边长结构+双向链表,但对其内部做了优化,比如元素之间相互独立,无级联更新影响、提供了新的方式双向遍历、占用内存比ziplist更少,ziplist基础结构为11byte,lisypack基础结构为7byte、去除了pre_e_l而使用length边长结构,pre固定为1|5字节,而length可以为1、2、3、4、5byte,更加节省内存。
图来源于
struct lpentry {
int encoding; 结构编码类型
optional byte[] content;
int length; length放在尾部,是一个固定长度编码的元素。可以通过这个长度和total_bytes计
算出尾部元素的偏移量。
}
length:
长度字段使用 varint 进行编码,不同于 skiplist 元素长度的编码为 1 个字节或者 5 个字节,listpack 元素长度的编码可以是 1、2、3、4、5 个字节。同 UTF8 编码一样,它通过字节的最高位是否为 1 来决定编码的长度。
encoding:
Redis 为了让 listpack 元素支持很多类型,它对 encoding 字段也进行了较为复杂的设计--压缩编码。支持多种长度的数据类型:3种字符串结构、6种整形结构。
首先listpack中存储了total_bytes 和每个元素的length,首先total_bytes-lend(固定1字节)指向了最后一个元素的末尾地址,因为length存储在entry尾部并且没有做特殊编码,可以很容易的解析最后length字段,拿到最后一个元素的占用空间,然后total_bytes-lend-tail_length既尾元素的首地址。
skiplist本质上也是一种查找结构,用于解决算法中的查找问题(Searching),即根据给定的key,快速查到它所在的位置(或者对应的value)。实际上,它是在有序链表的基础上发展起来的。除了最下面第1层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针故意跳过了一些节点(而且越高层的链表跳过的节点越多)。这就使得我们在查找数据的时候能够先在高层的链表中进行查找,然后逐层降低,最终降到第1层链表来精确地确定数据位置。在这个过程中,我们跳过了一些节点,从而也就加快了查找速度。
Skiplist是一种查找结构,主要解决范围查询和索引查询,传统双向链表无法提供这样的功能,而平衡树结构的实现复杂,占用内存多。Skiplist使用一种跳层的思想,有O(logn)的时间复杂度,内部维护了多条有序链表。
Skiplist内部维护了多条双向链表,除第一层的有序排列的全部元素外,还维护了多层稀疏的有序链表,稀疏链表每一层都会底层的一些节点,在查找时会从高到低逐层查找,类似于二分法思想,从而实现快速查找元素。
如果使用固定关系的层数,比如每X个节点之间建立一个高层,当对链表做调整时,可能导致链表结构层级变动,因为固定关系的限制会导致大部分节点的引用关系或者层级关系变动,所以采用了随机层数。随机层数是使用随机算法为元素随机分配一个层级,只需要记录前向和后向元素即可,修改时也只改动相邻元素的引用关系,而不会导致整个结构做修改。随机层数降低了元素插入、删除的复杂度,这是skiplist的一个重要特性,也是优于平衡树的原因。
Redis中的Zset是在skiplist, dict和ziplist基础上构建起来的:
1.当数据较少时,sorted set是由一个ziplist来实现的。
ziplist就是由很多数据项组成的一大块连续内存。由于sorted set的每一项元素都由数据和score组成,因此,当使用zadd命令插入一个(数据, score)对的时候,底层在相应的ziplist上就插入两个数据项:数据在前,score在后。
ziplist的主要优点是节省内存,但它上面的查找操作只能按顺序查找(可以正序也可以倒序)。因此,sorted set的各个查询操作,就是在ziplist上从前向后(或从后向前)一步步查找,每一步前进两个数据项,跨域一个(数据, score)对。
随着数据的插入或元素太大,Redis会将zset的ziplist结构转为skiplist结构,临界点为:
zset-max-ziplist-entries 128 :最多存储128个元素,既256个数据项(因为score和value占用两个位置)
zset-max-ziplist-value 64 :任意一个数据长度超过64k
2.当数据多的时候,sorted set是由一个叫zset的数据结构来实现的,这个zset包含一个dict + 一个skiplist。dict用来查询数据到分数(score)的对应关系,而skiplist用来根据分数查询数据(可能是范围查找)。
Zset的实现:
当数据较少时,sorted set是由一个ziplist来实现的。
当数据多的时候,sorted set是由一个dict + 一个skiplist来实现的。简单来讲,dict用来查询数据到分数的对应关系,而skiplist用来根据分数查询数据(可能是范围查找)
zset的skiplist内部包含两种结构,虽然同时使用两种结构,但它们会通过指针来共享相同元素的member和score,因此不会浪费额外的内存,如下:
1.dict 通过数据来找到分数,时间复杂度O(1)
2.skiplist 通过分数|排名来找数据,时间复杂度O(logn)
Redis的跳跃链表一共64层,容纳元素数量为极大,Header头结点,key-score 为最低值,value为null,每一次的遍历都是从header开始的元素与元素之间使用双向链表连接,他们是有序排列的,从小到大排列,不同的kv元素层高不一样,层数越高的kv元素越少。结构如下:
zset数据结构:
typedef struct zskiplistNode {
robj *obj; key的RedisObject引用,和dict中存储的元素共享引用
double score; 数据对应的分数
struct zskiplistNode *backward; 指向链表前一个节点的指针(前向指针)。节点只有1个前向
指针,所以只有第1层链表是一个双向链表。
struct zskiplistLevel {
struct zskiplistNode *forward; 每层对应1个后向指针,用forward字段表示
unsigned int span; 每个后向指针还对应了一个span值,它表示当前的指针跨越了多少个
节点。span用于计算元素排名(rank),这正是前面我们提到的Redis对于skiplist所做的一个扩展
} level[]; level[]存放指向各层链表后一个节点的指针(后向指针)。每层对应1个后向指针,
用forward字段表示
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; 头指针header和尾指针tail。
unsigned long length; 链表长度length,即链表包含的节点总数。注意,新创建的skiplist包
含一个空的头指针,这个头指针不包含在length计数中。
int level; 表示skiplist的总层数,即所有节点层数的最大值。
} zskiplist; zskiplist定义了真正的skiplist结构
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
Skiplist的内容和实现还有更多内容,欢迎阅读Redis专栏。
参考:Redis内部数据结构详解(6)——skiplist: http://zhangtielei.com/posts/blog-redis-skiplist.html