Redis高效之数据结构设计

Redis数据结构

1.redis为啥这么快呢?

  • 内存数据库

  • 高效数据结构

  • 单线程模式

2.高效的数据结构指的是什么?

redis的数据结构String(字符串),List(列表),Hash(哈希),Set(集合)和Sorted Set(有序集合)。

3.这些结构底层是个啥原理呀?

靠腰,这些我也知道呀,这是redis提供的数据结构呀。它们底层是个啥实现原理呢?

别急,接下来我们就说说redis的底层数据结构,底层数据结构一共有6种,分别是,简单动态字符串,双向链表,压缩列表,哈希表,跳表和整数数组,它们和数据类型的对应关系如下图所示:

可以看到,String 类型的底层实现只有一种数据结构,也就是简单动态字符串。而 List、Hash、Set 和 Sorted Set 这四种数据类型,都有两种底层实现结构。通常情况下,我们会把这四种类型称为集合类型,它们的特点是一个键对应了一个集合的数据。

4.理解redis高效的几个根本疑惑

嗯,好像很厉害的样子,但是我没听太懂?有几个疑惑

  1. 你现在介绍的都是redis的值的底层结构,要说redis高效,应该指的是两部分:

    • 从键查找值的速度快

    • 如果值是集合结构的话,从集合中检索到数据也得快

    所以你首先应该告诉我键->值之间的结构关系,为啥从键查找到值的速度这么快呢?

  2. 说了这么多的底层结构,你也没说他们为啥就快呀?

  3. 还有这个简单动态字符串是个什么玩意

5.redis高效的原因,解惑

别急呀,你的问题确实都很关键,我们一个个看啊。

5.1键和值用什么结构组织?

这个其实很好猜的,高效的k-v查询结构,肯定是hash表没跑了,那其中又有啥不一样呢?

没错redis的k-v存储结构就是hash表,而且是数组实现的hash表。数组的每一个元素我们叫一个hash桶,hash桶中的entry元素存储了*key*value指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过*value指针被查找到。

应为这个哈希表保存的所有的键值对,所以我们把它叫做全局哈希表。

但是只要是哈希表就会有哈希冲突和rehash可能带来的操作阻塞?哪redis是怎么做的呢?

首先解决hash冲突的问题,redis也是通过链式hash的结构来解决hash冲突的。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。

然后解决链式哈希时间复杂度从O(1)退变为O(N)的问题,这个就得依赖rehash机制了,那redis的rehash机制是啥样的呢?

为了让rehash操作更高效,redis使用了两个全局哈希表,这个过程分为三步:

  1. 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;

  2. 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;

  3. 释放哈希表 1 的空间。

这样看起来好像没啥问题了,但是我们遗漏了第二步骤,如果把哈希表1的数据一次性都拷贝到哈希表2中,很可能会造成redis线程阻塞,无法响应其他请求。

所以redis使用了渐进式rehash。那这又是个啥玩意呢?

简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。如下图所示:

这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

好了,到这里,你应该就能理解,Redis 的键和值是怎么通过哈希表组织的了。对于 String 类型来说,找到哈希桶就能直接增删改查了,所以,哈希表的 O(1) 操作复杂度也就是它的复杂度了。

但是,对于集合类型来说,即使找到哈希桶了,还要在集合中再进一步操作。这里就呼应我们提到的底层存储结构,优秀的底层存储结构就是保证在集合中检索到具体数据的速度够快。

5.2集合数据的操作效率为啥快?

嗯,我现在知道为什么redis从键找到值的速度很快了,那么我们找到集合后,怎么保证集合中查找数据够快呢?

问的好,理解从键找到值的速度为啥快的问题,说明你跟上了,我们接下来看下一个问题,如果上面的问题你还不知道为啥,你该回去在看看上面的内容了。

和 String 类型不同,一个集合类型的值,第一步是通过全局哈希表找到对应的哈希桶位置,第二步是在集合中再增删改查。那么,集合的操作效率和哪些因素相关呢?为啥他就快呢?

首先,与集合的底层数据结构有关。例如,使用哈希表实现的集合,要比使用链表实现的集合访问效率更高。其次,操作效率和这些操作本身的执行特点有关,比如读写一个元素的操作要比读写所有元素的效率高。

关于整数数组,双向链表和哈希表我们就没啥好说的了,相信大家都知道他们的特点。

而压缩列表和跳表的结构就需要说一说了

压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。

在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。

我们再来看下跳表。有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。具体来说,跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:

好了,我们现在可以按照查找的时间复杂度给这些数据结构分下类了:

知道底层数据结构后呢,我们还要明确不同操作类型,其效率也是不同的。

集合类型的操作类型很多,有读写单个集合元素的,例如 HGET、HSET,也有操作多个元素的,例如 SADD,还有对整个集合进行遍历操作的,例如 SMEMBERS。这么多操作,它们的复杂度也各不相同。而复杂度的高低又是我们选择集合类型的重要依据。我总结了一个“四句口诀”,希望能帮助你快速记住集合常见操作的复杂度。这样你在使用过程中,就可以提前规避高复杂度操作了。

  • 单元素操作是基础;

  • 范围操作非常耗时;

  • 统计操作通常高效;

  • 例外情况只有几个。

第一,单元素操作,是指每一种集合类型对单个数据实现的增删改查操作。例如,Hash 类型的 HGET、HSET 和 HDEL,Set 类型的 SADD、SREM、SRANDMEMBER 等。这些操作的复杂度由集合采用的数据结构决定,例如,HGET、HSET 和 HDEL 是对哈希表做操作,所以它们的复杂度都是 O(1);Set 类型用哈希表作为底层数据结构时,它的 SADD、SREM、SRANDMEMBER 复杂度也是 O(1)。

第二,范围操作,是指集合类型中的遍历操作,可以返回集合中的所有数据。这类操作的复杂度一般是 O(N),比较耗时,我们应该尽量避免。

第三,统计操作,是指集合类型对集合中所有元素个数的记录,例如 LLEN 和 SCARD。这类操作复杂度只有 O(1),这是因为当集合类型采用压缩列表、双向链表、整数数组这些数据结构时,这些结构中专门记录了元素的个数统计,因此可以高效地完成相关操作。

第四,例外情况,是指某些数据结构的特殊记录,例如压缩列表和双向链表都会记录表头和表尾的偏移量。这样一来,对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂度也只有 O(1),可以实现快速操作。

6.思考

整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,那为什么 Redis 还会把它们作为底层数据结构呢?

两方面原因:

1、内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少。Redis是内存数据库,大量数据存到内存中,此时需要做尽可能的优化,提高内存的利用率。

2、数组对CPU高速缓存支持更友好,所以Redis在设计时,集合数据元素较少情况下,默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度。当数据元素超过设定阈值后,避免查询时间复杂度太高,转为哈希和跳表数据结构存储,保证查询效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值