【redis进阶之路】redis工作原理之底层数据结构(二)

175 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第23天,点击查看活动详情

0、引言

上期我们讲述了redis的IO多路复用器epoll,这期我们接着谈谈redis的底层数据结构

1、redis数据类型的底层数据结构

这里只是简单的述说了redis中常用数据类型的底层数据结构,常说redis有5中数据类型:String,list,set,hash,zset。但是千万不要以为只有这5中数据结构,其他的可以自己做了解,这里单独提一下位图(bitmap)数据类型,针对有规律且连续的大量数据,用位图来存储可以实现很小的空间占用存储很大的数据量

(1)针对String的动态字符串(Simple Dynamic String)

redis是基于C语言开发的,在C语言中针对字符串的处理是比较耗费资源的,主要问题在于:

a.容易造成缓冲区溢出:字符串底底层是用了一个数组来存储的,如果在拼接的时候没有计算好内存空间,拼接进去的字符串比剩下的空间要大,就导致了溢出

b.要获取字符串长度,就要遍历一遍,复杂度是O(N)

c.内存重新配置:字符串变长/短都会对数组作内存重分配 针对上述问题redis实现了自己的SDS字符串结构——SDS:

struct sdshdr{
  int len;  // 字符串长度/已使用的空间的长度
  int free; // bug中空闲空间长度
  char buf[]; // 存储的实际内容
}

具体怎么优化的?针对上述3个问题我们一个一个来看

a.针对溢出问题,redis中是有free记录的,拼接的时候看一下free够不够,不够就扩容,就避免了溢出了

b.可以直接通过len获取到字符串长度而不用每次都遍历

c.减少了内存的重新配置。SDS提供了两种优化策略:空间预分配和惰性空间释放 空间预分配:当扩容时除了分配必需的内存空间还会额外分配未使用的空间:如果字符串大小小于1M时,直接扩大一倍,如果大于1M,那就扩大1M的空间 惰性空间释放:缩容时,不回收多余的内存空间,而是用free记录下多余的空间,后续再有操作直接使用free的空间

(2)针对hash的字典结构

所谓字典结构,其实就是键值对结构,redis中的字典是用的哈希表来实现(可以与hashMap联想理解,同时这里介绍字典概念是为了理解后续的压缩链表)。c中是没有哈希表结构,所以这里的哈希表也是redis自己实现的。既然用了hash,那不可避免的就涉及到哈希冲突的问题。redis是用了拉链法来解决,即引入一个链表来盛装冲突的元素,同时当哈希表太大或太小时就要开始扩容/缩容了

首先与hashMap类似,redis的扩容所容(rehash)也是采用了扩大一倍或者缩小一倍的操作。目的是为了让容量保持为2^x,这样就可以使用按位运算来代替取模运算,即让N%length==N&(leng-1)成立

截止到上述,并没有说到redis处理亮点的地方。redis的一个优化细节,就在于为了保持高效,采用了渐进式rehash,也就是说扩容缩容不是一次性完成的,而是多次渐进完成的,因为当数据有百万量级时一次性完成扩容/缩容必定导致redis无法做别的工作,而渐进式rehash的做法是在rehash期间底层维护了两个哈希表,一个主一个备,查找删除更新都会在两个哈希表上进行,先找主,主没有再找备;新增是在备上进行的,同时扩容也是在备上进行,这样就能把扩容的压力放到备上,主就专心提供当前服务

【redis的快在于很多的实现细节,要理解他的快,就要理解这些细节上的亮点】

(3)针对sort set的跳表

跳跃表的结构是也是redis底层结构的亮点之一。对于redis的sort set要保证两点:查询快、支持排序

这里先引入一个思考:你所知道的满足这两条的数据结构有哪些?


1、AVL树 2、红黑树 3、B树,B+树 4、跳表 首先要理解上述的这些数据结构,如果不知道这些结构的可以先了解后再往后看

1、首先AVL树有旋转操作,其最高子树与最矮子树高度差不能超过1,它通过插入节点时旋转来使后续的查询操作的效率更高,也就是说通过新增节点时增加成本,来减少查询节点的成本。也就是说明AVL树适合查多增删的场景,那如果增删一样多呢?同时因为高度差不能超过1,随着节点数增加,不可避免的,整棵树还是会越来越高,树越高就会导致IO量越大,查询效率就越低,怎么办?

2、引入了红黑树,红黑树可以旋转来调整节点,也可以引入了变色,同时其容纳的高度差在两倍以内,也就是说最高子树不超过最矮树高度的两倍就可以了。这样就让查询性能和插入节点的性能得到了一个近似的平衡

3、但是因为红黑树是二叉的,随着数据量越来越大,不可避免的一层能装的节点始终有限,树还是会越来越高。引入了有序多叉树——B树,因为B树在每个节点中会盛装key和value,那么一个节点能装的数据就很有限,为了提高节点能盛装的数据量,又引入了B+树,非叶子节点只装key,只有叶子节点才会把所有的key和value再盛装下

到这里就简单的描述了下这些树的演变,按上面所说的,B+树是最能装的了,

那么为什么不用B+树而用跳表呢? 通过上面的描述可以知道B+树引入的目的就在于尽可能的让树矮一点,每个节点装的数据多一点,这样去查找数据时的IO量就更少,核心目的在于减少IO,那么要理解IO是什么?简单来说IO就是计算机的核心(CPU,内存)与其他设备之间数据转移的过程,比如数据从磁盘读取到内存,或者从内存写入到磁盘,都是IO操作。而redis的本质是什么,就是基于内存操作的,所以对他而言,是读写数据是没有IO的(除非持久化操作),那就没有必要使用B+树呀。

那为什么不用红黑树呢? redis追求的核心就是查询快,更加适用于查多改少的场景。 1、红黑树的实现是需要旋转、变色等操作的,是要耗费性能的,而跳表的实现更加简单,就修改、删除数据而言效率更高,只需要维护前后节点,如果节点还没选为了上层节点,那么还需要再维护一些上层节点的前后节点,但都比红黑树更简单 2、跳表的结构决定了,其范围查询的效率比红黑树更高。跳表通过上层链表可以很快的定位到数据范围,所以针对范围查询效率很高 3、查找单个key时,跳表和红黑树的时间复杂度都是O(logN)

综上所诉,选择了跳表

【拓展】有兴趣再考虑一下以下问题,可以帮助你理解这些数据结构的选型:

1、HashMap 在jdk1.8后链表长度超过8会转换为红黑树,为什么这里用红黑树不用B+树?

2、HashMap为什么用红黑树不用跳表?

3、mysql存储引擎的索引数据结构为什么用B+树而不用红黑树?

(4)针对list的压缩列表

压缩列表ziplist是列表键(list)和字典键(hash)的底层实现之一。是由特殊编码的内存块组成的列表。它的亮点就在于内存是连续分配的,也就是说这些列表中的元素在物理内存中都是挨着的,那么当遍历时其速度就非常快。