认真写文章,用心作分享。redis
我的网站:yasinshaw.com数组
公众号:xy的技术圈数据结构
之前在使用Redis的时候,只是简单地使用它提供的基本数据类型和接口,并无深刻研究它底层的数据结构。最近打算从新学习梳理一下Redis方面的知识,因此打算从介绍Redis的基本类型及其数据结构入手。函数
Redis的key是顶层模型,它的value是扁平化的。Redis中,全部的value都是一个object,它的结构以下:学习
typedef struct redisObject {
unsigned [type] 4;
unsigned [encoding] 4;
unsigned [lru] REDIS_LRU_BITS;
int refcount;
void *ptr;
} robj;
复制代码
简单介绍一下这几个字段:优化
在Redis内部,string类型有两种底层储存结构。Redis会根据存储的数据及用户的操做指令自动选择合适的结构:网站
SDS: 简单动态字符串 simple dynamic stringui
SDS的内部数据结构:编码
typedef struct sdshdr {
// buf中已经占用的字符长度
unsigned int len;
// buf中剩余可用的字符长度
unsigned int free;
// 数据空间
char buf[];
}
复制代码
可见,其底层是一个char数组。buf最大容量为512M,里面能够放字符串、浮点数和字节。因此你甚至能够放一张序列化后的图片。它为何没有直接使用数组,而是包装成了这样的数据结构呢?spa
由于buf会有动态扩容和缩容的需求。若是直接使用数组,那每次对字符串的修改都会致使从新分配内存,效率很低。
buf的扩容过程以下:
惰性空间释放指的是当字符串缩短时,并无真正的缩容,而是移动free的指针。这样未来字符串长度增长时,就不用从新分配内存了。但这样会形成内存浪费,Redis提供了API来真正释放内存。
list底层有两种数据结构:链表linkedlist和压缩列表ziplist。当list元素个数少且元素内容长度不大时,使用ziplist实现,不然使用linkedlist。
Redis使用的链表是双向链表。为了方便操做,使用了一个list结构来持有这个链表。如图所示:
typedef struct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void *(*dup)(void *ptr);
//节点值释放函数
void *(*free)(void *ptr);
//节点值对比函数
int (*match)(void *ptr,void *key);
}list;
复制代码
data存的其实也是一个指针。链表里面的元素是上面介绍的string。由于是双向链表,因此能够很方便地把它当成一个栈或者队列来使用。
与上面的链表相对应,压缩列表有点儿相似数组,经过一片连续的内存空间,来存储数据。不过,它跟数组不一样的一点是,它容许存储的数据大小不一样。每一个节点上增长一个length属性来记录这个节点的长度,这样比较方便地获得下一个节点的位置。
上图的各字段含义为:
压缩列表不仅是list的底层实现,也是hash的底层实现之一。当hash的元素个数少且内容长度不大时,使用压缩列表来实现。
hash底层有两种实现:压缩列表和字典(dict)。压缩列表刚刚上面已经介绍过了,下面主要介绍一下字典的数据结构。
字典其实就相似于Java语言中的Map
,Python语言中的dict
。与Java中的HashMap
相似,Redis底层也是使用的散列表做为字典的实现,解决hash冲突使用的是链表法。Redis一样使用了一个数据结构来持有这个散列表:
在键增长或减小时,会扩容或缩容,而且进行rehash,根据hash值从新计算索引值。那若是这个字典太大了怎么办呢?
为了解决一次性扩容耗时过多的状况,能够将扩容操做穿插在插入操做的过程当中,分批完成。当负载因子触达阈值以后,只申请新空间,但并不将老的数据搬移到新散列表中。当有新数据要插入时,将新数据插入新散列表中,而且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,都重复上面的过程。通过屡次插入操做以后,老的散列表中的数据就一点一点所有搬移到新散列表中了。这样没有了集中的一次一次性数据搬移,插入操做就都变得很快了。这个过程也被称为渐进式rehash。
set里面没有重复的集合。set的实现比较简单。若是是整数类型,就直接使用整数集合intset。使用二分查找来辅助,速度仍是挺快的。不过在插入的时候,因为要移动元素,时间复杂度是O(N)。
若是不是整数类型,就使用上面在hash那一节介绍的字典。key为set的值,value为空。
zset是可排序的set。与hash的实现方式相似,若是元素个数很少且不大,就使用压缩列表ziplist来存储。不过因为zset包含了score的排序信息,因此在ziplist内部,是按照score排序递增来存储的。意味着每次插入数据都要移动以后的数据。
跳表(skiplist)是另外一种实现dict的数据结构。跳表是对链表的一个加强。咱们在使用链表的时候,即便元素的有序排列的,但若是要查找一个元素,也须要从头一个个查找下去,时间复杂度是O(N)。而跳表顾名思义,就是跳跃了一些元素,能够抽象多层。
以下图所示,好比咱们要查找8,先在最上层L2查找,发如今1和9之间;而后去L1层查找,发如今5和9之间;而后去L0查找,发如今7和9之间,而后找到8。
当元素比较多时,使用跳表能够显著减小查找的次数。
同list相似,Redis内部也不是直接使用的跳表,而是使用了一个自定义的数据结构来持有跳表。下图左边蓝色部分是skiplist,右边是4个zskiplistNode。zskiplistNode内部有不少层L一、L2等,指针指向这一层的下一个结点。BW是回退指针(backward),用于查找的时候回退。而后下面是score和对象自己object。
Redis对外暴露的是对象(数据类型),而每一个对象都是用一个redisObject持有,经过不一样的编码,映射到不一样的数据结构。从最开始的那个图能够知道,有时候不一样对象可能会底层使用同一种数据结构,好比压缩列表和字典等。
在了解数据结构后,咱们就可以更清楚应该选用什么样的对象,出现问题时应该如何优化了。
本文主要参考了博客主“崖边小生”的Redis系列文章,感谢做者。博客连接:
关注公众号:xy的技术圈