Redis数据结构学习笔记

Redis数据结构学习笔记

常见的数据结构有数组、链表、集合、哈希表、二叉树、跳表等等,那么Redis里面有多少种数据类型,又是怎么实现的呢?

Redis的特点

  • 存储效率高。Redis是对于计算机资源的主要消耗就在于内存,而且内存成本较高,因此节省内存是它非常非常重要的一个方面。这意味着Redis一定是非常精细地考虑了压缩数据、减少内存碎片等问题。

  • 快速响应。与快速响应时间相对的,是高吞吐量。Redis是用于提供在线访问的,对于单个请求的响应时间要求很高,因此,快速响应时间是比高吞吐量更重要的目标。

  • 单线程。Redis的性能瓶颈在于内存访问和网络IO。采用单线程的设计带来的好处是,极大简化了数据结构和算法的实现。另外,Redis通过异步IO和pipelining等机制来实现高速的并发访问。

Redis数据类型

面向用户的类型

  • string
  • list 列表
  • hash 哈希表
  • set 集合
  • sorted set 有序集合

内部数据类型

  • dict
  • sds
  • ziplist
  • quicklist
  • skiplist

dict

dict是一个用于维护key和value映射关系的数据结构。是一个基于哈希表的算法。它采用某个哈希函数从key计算得到在哈希表中的位置,采用拉链法解决冲突,并在装载因子(load factor)超过预定值时自动扩展内存,引发重哈希(rehashing)。

增量式重哈希

将重哈希操作分散到对于dict的各个增删改查的操作中去。这种方法能做到每次只对一小部分key进行重哈希,而每次重哈希之间不影响dict的操作。
过程如下图所示,每插入/查找/删除一个数据,顺带从旧表拷贝一个数据到新表,把数据迁移过程分摊。
Redis数据结构学习笔记_第1张图片

sds (Simple Dynamic String)

特点

  • 可动态拓展内存
  • 二进制安全,能存储任意二进制数据,不仅仅是可打印字符
  • 与传统的c语言字符串类型兼容

ziplist

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 实现了双向指针的效果,棒棒哒!!!

最后是 zlend,表示ziplist结束
Redis数据结构学习笔记_第2张图片
重要的来了!

当hash field 和 value较少时,底层数据结构其实用的是 ziplist,数据量大了之后才用dict
为啥呢?主要原因如下

  • 数据量少,每次都全部遍历也没啥关系,还是很快的
  • ziplist特别省空间,dict还有个重载因子不能过大的问题,其实会浪费很多空间
  • ziplist空间连续,cpu读写快

注意:当数据量大了之后,全部遍历性能就会变很差,另外修改或删除数据,需要移动后面的数据,涉及到内存拷贝,会比较花时间,因此存在ziplist的数据最好不要删除或者修改。

quicklist

也就是Redis对外暴露的 list
先看看list支持哪些操作吧

  • lpush 左侧插入数据
  • lpop 左侧删除数据
  • rpush 右侧插入数据
  • rpop 右侧删除数据
    以上时间复杂度都是O(1),符合这个要求的,就是双向链表了。

上面介绍ziplist的时候,也说了双向链表的缺点,所以这里就不会完全用双向链表咯。

双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
ziplist由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。

双向链表的缺点,刚好ziplist可以解决;ziplist的缺点刚好双向链表可以解决,因此强强结合,把ziplist当成双向链表的一个节点,数据存储在ziplist中,这就是quicklist啦

这里就有个关键参数,一个节点(ziplist)存多少个数据比较合适呢?
多了的话,ziplist的缺点就被放大
少了的话,双向链表的缺点就被放大
Redis默认设置是 每个quicklist节点的ziplist大小不超过8k

list的设计目标是能够用来存储很长的数据列表的

skiplist

有序集合zset的主要实现方式
以下是跳表数据插入过程:

  • 先查找到应该插入在哪个位置
  • 随机函数生成层数,决定该数据有几层,该随机函数具有层数越高概率越低的特点
// 随机函数伪代码
randomLevel()
    level := 1
    // random()返回一个[0...1)的随机数
    while random() < p and level < MaxLevel do
        level := level + 1
    return level

Redis数据结构学习笔记_第3张图片
理想情况下,每一层的数据量都是下一层的一半,查询的时候从最高层往下查询,实现了O(logn)的查找效率。

zset 的实现

实际上,Redis中sorted set的实现是这样的:

  • 当数据较少时,sorted set是由一个ziplist来实现的。
  • 当数据多的时候,sorted set是由一个dict + 一个skiplist来实现的。简单来讲,dict用来查询数据到分数的对应关系,而skiplist用来根据分数查询数据(可能是范围查找)。
  • skiplist被改造了,一方面支持score重复,重复时根据key的字典序排;另一方面,还会记录距离前一个节点的数据间隔span,这就是用来计算排名的。
zset常用命令
  • zscore 查询分数
  • zrevrangebyscore 根据分数区间查询,并按从小到大排名
  • zrevrange 按照排名区间查询
  • zrank 查询排名

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;

Redis数据结构学习笔记_第4张图片

intset

一个由整数组成的有序集合,因此可以用二分查找法,实现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,以达到节省内存的目的
Redis数据结构学习笔记_第5张图片
intset与ziplist相比:

  • ziplist可以存储任意二进制串,而intset只能存储整数。
    ziplist是无序的,而intset是从小到大有序的。因此,在ziplist上查找只能遍历,而在intset上可以进行二分查找,性能更高。
    ziplist可以对每个数据项进行不同的变长编码(每个数据项前面都有数据长度字段len),而intset只能整体使用一个统一的编码(encoding)。

你可能感兴趣的:(Redis,redis,数据结构,有序集合,跳表)