常见的数据结构有数组、链表、集合、哈希表、二叉树、跳表等等,那么Redis里面有多少种数据类型,又是怎么实现的呢?
存储效率高。Redis是对于计算机资源的主要消耗就在于内存,而且内存成本较高,因此节省内存是它非常非常重要的一个方面。这意味着Redis一定是非常精细地考虑了压缩数据、减少内存碎片等问题。
快速响应。与快速响应时间相对的,是高吞吐量。Redis是用于提供在线访问的,对于单个请求的响应时间要求很高,因此,快速响应时间是比高吞吐量更重要的目标。
单线程。Redis的性能瓶颈在于内存访问和网络IO。采用单线程的设计带来的好处是,极大简化了数据结构和算法的实现。另外,Redis通过异步IO和pipelining等机制来实现高速的并发访问。
面向用户的类型
内部数据类型
dict是一个用于维护key和value映射关系的数据结构。是一个基于哈希表的算法。它采用某个哈希函数从key计算得到在哈希表中的位置,采用拉链法解决冲突,并在装载因子(load factor)超过预定值时自动扩展内存,引发重哈希(rehashing)。
将重哈希操作分散到对于dict的各个增删改查的操作中去。这种方法能做到每次只对一小部分key进行重哈希,而每次重哈希之间不影响dict的操作。
过程如下图所示,每插入/查找/删除一个数据,顺带从旧表拷贝一个数据到新表,把数据迁移过程分摊。
特点
ziplist是一个经过了特殊编码的双向链表,设计目的就是为了提高存储效率,但是本质上其实是个list,一个ziplist整体占用一大块内存,一方面便于存取,另一方面不会导致大量的内存碎片(传统的链表,通过指针链接,空间不连续,碎片会很多,也影响存取效率)
但是又为什么说是一种双向链表呢?来看下ziplist是怎么存储数据的。
请看下图,ziplist连续空间,
开头,是4字节的 zlbytes,表示ziplist占用的字节数;
接下来的,是 zltail 表示最后一项数据(entry)在ziplist中的偏移字节数,这个是为了快速取到最后一项数据,相当于双向链表的尾指针;
再下来,是zllen 表示 ziplist中的数据项个数;
然后,就是数据entry了,一个entry又分成了三部分,分别是 prevrawlen,len 和 data
prevrawlen:上一个entry的大小,可以通过这个快速定位上一个entry的偏移量,也就相当于指向前一个数据的指针;
len:表示当前数据(data)的长度,因为数据是连续存储的,这样也就知道了下一个entry的入口,相当于指向下一个数据的指针;
data:实际数据咯
通过 prevrawlen 和 len 实现了双向指针的效果,棒棒哒!!!
当hash field 和 value较少时,底层数据结构其实用的是 ziplist,数据量大了之后才用dict
为啥呢?主要原因如下
注意:当数据量大了之后,全部遍历性能就会变很差,另外修改或删除数据,需要移动后面的数据,涉及到内存拷贝,会比较花时间,因此存在ziplist的数据最好不要删除或者修改。
也就是Redis对外暴露的 list
先看看list支持哪些操作吧
上面介绍ziplist的时候,也说了双向链表的缺点,所以这里就不会完全用双向链表咯。
双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
ziplist由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。
双向链表的缺点,刚好ziplist可以解决;ziplist的缺点刚好双向链表可以解决,因此强强结合,把ziplist当成双向链表的一个节点,数据存储在ziplist中,这就是quicklist啦
这里就有个关键参数,一个节点(ziplist)存多少个数据比较合适呢?
多了的话,ziplist的缺点就被放大
少了的话,双向链表的缺点就被放大
Redis默认设置是 每个quicklist节点的ziplist大小不超过8k
list的设计目标是能够用来存储很长的数据列表的
有序集合zset的主要实现方式
以下是跳表数据插入过程:
// 随机函数伪代码
randomLevel()
level := 1
// random()返回一个[0...1)的随机数
while random() < p and level < MaxLevel do
level := level + 1
return level
理想情况下,每一层的数据量都是下一层的一半,查询的时候从最高层往下查询,实现了O(logn)的查找效率。
实际上,Redis中sorted set的实现是这样的:
zscore 通过dict查找,时间复杂度为O(1)
zrevrangebyscore 是根据分数查询,采用skiplist,时间复杂度O(logn)
zrank 排名查询,略微复杂,先通过dict查找score,再通过score查找skiplist中的位置O(logn),最后通过span(下图中带()的线)计算排名O(logn)
zskiplist 的数据结构定义
#define ZSKIPLIST_MAXLEVEL 32 // 最大层级
#define ZSKIPLIST_P 0.25 // 理想情况下,第n层和第n-1层的节点数之比
typedef struct zskiplistNode {
robj *obj; // 数据
double score; // 分数
struct zskiplistNode *backward; // 后项指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 前向指针
unsigned int span; // 距离前一个节点的间隔,用来计算排名
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
一个由整数组成的有序集合,因此可以用二分查找法,实现O(logn)的查询效率
// 数据结构定义
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))
contents 存储数据,会自动根据数据大小来调节encoding,以达到节省内存的目的
intset与ziplist相比: