redis中的数据结构和快速的redis有哪些慢动作

在这里插入图片描述

为啥 Redis 能有这么突出的表现呢?一方面,这是因为它是内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快。另一方面,这要归功于它的数据结构。这是因为,键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,所以高效的数据结构是 Redis 快速处理数据的基础。

简单来说,Redis底层结构有5种,分别是简单动态字符串、压缩列表、双向链表、哈希表、跳表和整数数组。
Redis数据类型对应底层数据结构
可以看到String就对应简单动态字符串,其他的都有两个底层数据结构。
这里先把简单动态字符串放到下一篇讲解。

哈希表

Redis为了实现快速通过键找到值,就是通过哈希表实现的。

一个哈希表相当是一个数组,数组中的每一个元素就是一个哈希桶,一个哈希表有多个哈希桶,每个哈希桶中保存了键值对。

这里的哈希桶存储的不是值本身,而是指向具体值的指针。不管是String,还是集合类型的都是一样。

下图描述了哈希桶中存储的entry对象中保存了key和value指针,以及发生哈希冲突的next指针,然后分别指向了真实的键和值,这样不管是集合还是String都可以通过value指针找到。
哈希表描述图
一个哈希表存储了所有的键值对,只需要计算一次hash值就可以以O(1)的时间复杂读快速定位它所对应的哈希桶位置,然后就可以访问相应的 entry 元素。

不管哈希表存储了多少哈希桶就算有10万也好20万也罢,他都会计算一次哈希找到具体的哈希桶。
这其实是因为你忽略了一个潜在的风险,那就是哈希表的冲突问题和 rehash 可能带来的操作阻塞。

redis中的哈希表为什么慢怎么解决呢?

如果哈希表中存了很多的数据,就难免不会发生哈希冲突,也就是说计算哈希值两个key算出来都是一样的,这样就会争抢一个哈希桶。
一般来说哈希桶的个数要小于key的个数,也就是说有些key都放到一个哈希桶中了。

Redis解决哈希冲突的办法就是链式哈希,也就是链表。当计算出相同的hash值时就会看哈希桶中有不有数据了,如果有,这时候就是冲突了,冲突了就会把之前占有哈希桶的数据往下移动,再把新的数据放到哈希桶中,然后通过next指针指向之前下移的数据。
如上图所示的entry对象一样。这样不管哈希桶中有多少数据,entry对象都可以把它链接起来。

但是这里还有问题,就是如果我的链表越来越长,随之查找的速度也就会越来越慢。因为要逐个遍历链表中的数据才能找到对应的值。

为了解决这个问题Redis又想出来一招,rehash操作,rehash 就是增加现有的哈希桶数量,让逐渐增多的 entry 对象能在更多的桶之间分开保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。

rehash其实就是对现有的哈希表进行复制到另一个hash表中,Redis会默认开启两个哈希表,称为哈希表1、哈希表2。插入数据时默认采用的时哈希表1,哈希表2还没有被使用到,当桶中的entry对象渐渐变多的时候,rehash会采用一下操作:

  • 给哈希表2分配空间,比如时哈希表1大小的一两倍左右;
  • 把哈希表1中的数据复制到哈希表2中;
  • 最后把哈希表1的空间释放掉。

其实就是用更大的哈希表进行装载,把之前小的哈希表1分散开来装到哈希表2中。

但是, 这个 rehash 动作并不是一次性、集中式地完成的, 而是分多次、渐进式地完成的。
这样做的原因是,如果键值对只有4、5对的话,redis可以瞬间进行rehash。但是如果是一百万,两百万,甚至是上亿的时候,那么要一次性将这些键值对全部 rehash 到哈希表2 的话, 庞大的计算量可能会导致服务器在一段时间内停止服务。

为了避免这个问题redis采用了渐进式rehash。

渐进式rehash

为了解决这个问题,redis不是一次性将数据进行拷贝到哈希表2,而是渐渐的,分多次的,渐进式的将哈希表1的数据慢慢慢慢的rehash到哈希表中。
可以分为以下几步:

  • 为哈希表2分配空间,让redis同时持有两个哈希表;
  • 再redis中维护一个索引计数器rehashindex,并将它的值设置为0,表示rehash开始工作;
  • rehash进行期间,每次对redis执行一次增删改查都会触发rehash把计数器等于0的哈希桶中的数据进行rehash到哈希表2中,程序再将计数器自增1次。
  • 随着操作不断,计数器的值也就不断上升。最终计数器就会自增到哈希表1的最后一个位置,这时候再将计数器的值设置为-1,表示rehash工作完成。

渐进式rehash的好处就是,把复制数据分而治之的平均分摊到对redis的所有操作之上,从而避免了集中式 rehash 而带来的庞大计算量。
看图方便理解,注意的是随着对数据的操作而产生的rehash

  • 准备rehash,准备两个桶,和计数器;
    准备rehash
  • 将计数器0的哈希桶rehash到哈希桶2中,这个过程可以分散到哈希表2中随机的哈希桶中。
    第一次rehash
  • 知道最后rehash到最后一个哈希桶。
    在这里插入图片描述
    最后将计数器设置成-1表示完成,这就是基本的渐进式rehash操作了。

压缩列表

redis是使用的字节数组表示一个压缩列表,如下图:
压缩列表示意图

  • zlbytes:压缩列表的字节长度,占4个字节,因此压缩列表最多有2^32-1个字节;
  • zltail:列表尾的偏移量,占4个字节;
  • zllen:列表长度,占两个字节;
  • entry:压缩列表存储的元素,可以是字节数组或者整数,长度不限;
  • zlend:压缩列表的结尾,占1个字节。

在压缩列表中,我们查找第一个元素和最后一个元素,可以通过字段的偏移量或者长度可以直接定位,时间复杂度是O(1)。查找其它元素是,要一个一个遍历,这时候时间复杂度就是O(n)。

跳表

接下来就是最常见的跳表。
跳表其实就是有序链表,只不过是吧有序链表进行了分层。
可以看一下有序链表:
有序链表
如果我要查询61这个值,就需要依次的遍历1->3->17->21->46->51-61,这个过程有7次比较加遍历。这个时间复杂度是O(n)的。
然后我们将有序链表进行分层,也可以说是加一层索引:
一层的索引跳表
从第一个元素开始每次随机选出一个元素作为链表,通过next指向下一个链表。比如我们要找到61这个元素:

  • 我们需要从1开始找到下一个3判断它是不是比61小,如果是继续往下;
  • 找到21判断它是不是比61小,是就继续;
  • 找到51判断它是不是比61小,是就继续找next指向的下一个元素;
  • 找到71,发现它比61大,所以从51节点往下找,找到next指向61,最终就找到了61这个元素。

总共查找6次就可以找到61节点,比有序链表少1次。当数据量大时,优势会更明显。
为了看的明显优势,我们在加一层:
两层
可以看的从1->21->51->61这一次我们只需要了4次就定位到61了。但数据量很大的时候,跳表的查找时间复杂度就是O(logN)。

跳跃表每个节点维护了多个指向其他节点的指针,所以在跳跃表进行查找、插入、删除操作时可以跳过一些节点,快速找到操作需要的节点。归根结底,跳跃表是以牺牲空间的形式来达到快速查找的目的。跳跃表与平衡树相比,实现方式更简单,只要熟悉有序链表,就可以轻松地掌握跳跃表。

小结

五大数据结构本章只描述了哈希表、压缩列表、跳表这三个,而双向链表和整数数组都太简单了。

  • 之所以能快速操作键值对,是因为O(1)的哈希表被广泛使用;
  • List类型的底层是压缩列表和双向列表。因此时间复杂度是O(N),建议是适当的使用List类型,毕竟压缩列表操作头尾的速度毕竟快,可以考虑用POP/PUSH的操作,效率相对毕竟高的,毕竟适合搞个队列,而不是随机读写;

掌握Redis底层数据结构很重要。

©️2020 CSDN 皮肤主题: 创作都市 设计师:CSDN官方博客 返回首页