Redis高性能的秘密原来在这,超越想象

Redis线程模型

        redis(Remote Dictionary Server)本质上是一个key-value型的数据库,整个数据库加载在内存当中运行定期通过异步操作把数据库数据flush到硬盘上,因此是纯内存操作。

        Redis是基于Reactor模式开发的网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构分为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列是单线程的,所以redis才叫做单线程模型。

        消息处理流程

        文件事件处理器使用I/O多路复用程序同时监听多个网络套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
        当被监听的套接字准备好连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就产生,这时文件事件处理器就会调用套接字之前关联好的事件来处理这些事件。

        尽管多个文件事件可能会并发的出现,但I/O多路复用程序总是会将所有产生事件的套接字都推到一个队列里面,然后通过这个队列,以有序、同步、每次一个套接字的方向向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后,I/O多路复用才会继续向文件事件配送器传送下一个套接字。

        Redis使用的是单线程模型,那为什么快呢?

Redis为什么快?

        Redis快体现在什么地方?它接收到键值对操作后,能以微妙级别的速度找到数据,并快速完成操作。数据库这么多,为什么Redis有这么突出的表现呢?一个重要原因是因为Redis快,那为什么Redis如此之快呢,主要有两个原因

  1. Redis是内存数据库,所有的操作都是在内存上完成,内存操作本来就快。
  2. 另一方面归功于它的数据结构。键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,所以高效的数据结构是Redis快速处理处理数据的基础。

        Redis值的数据结构常用的有5种:String、List、Hash、Set、Sorted Set。这里说的数据结构是它们底层是如何实现的。简单来说,底层数据结构一共有6种,分别是:简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。

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

        这些数据结构都是值的底层实现,键和值本身之间用什么结构组织的呢?为什么集合类型有那么多底层结构,他们都是怎么组织数据的,都很快吗?什么是简单动态字符串,和常用的字符串是一回事吗?

        全局Hash表

        为了实现从键到值的快速访问,Redis使用了一个哈希表来保存所有键值对。一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。所以我们常说,一个哈希表是由多个哈希桶组成的,每个哈希桶中键值对数据。看到这里可能会问,如果值是集合类型的话,作为数组元素的哈希桶怎么保存呢?其实哈希桶中的元素并不保存值本身,而是指向具体的指针。这也就是说,不管值是String,还是集合类型,哈希桶中的元素都是指向它们的指针。

        哈希桶中的entry元素保存了key和value指针,分别指向了实际的键和值,这样一来,即使是一个集合,也可以通过value指针被查到。

        因为这个哈希表保存了全部键值对,所以被称为全局哈希表。哈希表的最大好处是可以用O(1)的时间复杂度快速找到键值对。这个查找过程依赖于哈希计算,和数据量的多少没有直接关系。也就是说,不管是10万个键还是100万个键,我们只需要一次计算就能找到相应的键。

        如果只了解哈希表的O(1)复杂度快速查找特性,那么当往Redis中写入大量数据后,就可能发现操作突然变慢了,这其实是因为忽略了一个潜在的风险,那就是哈希冲突和rehash可能带来的操作阻塞。

        Hash表为何变慢了

        当向哈希表中写入更多数据时,哈希冲突是不可避免的问题。这里的哈希冲突指两个key的哈希值和哈希桶计算对应关系时,正好落在一个哈希桶中。毕竟哈希桶的数量通常要少于key的数量,也就是说,难免会有一些key的哈希值对应到同一个哈希桶中。

        解决哈希冲突的方法有两种,一是开放寻址法,二是拉链法。Redis使用的是第二个。

        entry1、ertry2、entry3都需要保存在哈希桶3中,导致哈希冲突,此时entry1元素会通过一个next指针指向entry2,同样entry2指向entry3,即使哈希桶3中有100个元素,也能把它们连接起来。

        但是,哈希冲突链上的元素只能通过指针逐一查找在操作。如果哈希表写入的数据越来越多,哈希冲突可能越来越多,这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率低。所以Redis会对哈希表做rehash操作。rehash就是增加现有哈希桶数量,让逐渐增多的entry元素能在更多的桶之间分散保存,减少单个桶中元素的数量,从而减少单个桶中的冲突。

        为了使rehash操作高效,Redis使用了两个全局哈希表:哈希表1和哈希表2,一开始刚插入数据时默认使用哈希表1,此时的哈希表2并没有分配空间。随着数据增多,Redis开始执行rehash,rehash的步骤:

  1. 给哈希表2分配更大的空间,如哈希表1的两倍
  2. 把哈希表1中的数据重新映射拷贝到哈希表2中
  3. 释放哈希表1的空间

        到此,可以从哈希表1切换到哈希表2,用更大的哈希表2保存更多的数据,而原来的哈希表1留作下一次rehash扩容。这个过程看似简单,但第二步涉及大量数据拷贝,如果一次性把哈希表1中的数据都迁移完,会造成Redis线程阻塞,无法服务其他请求。此时Redis就无法快速访问数据了。为了避免这个问题,Redis采用另外渐进式rehash。

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

        到这里应该能理解Redis的键和值是怎么通过哈希表组织的了。对于String类型来说,找到哈希桶就能直接操作了,所以,哈希表的O(1)操作复杂度就是它的复杂度。但是对于集合类型,即使找到了哈希桶,还要在集合中再进一步操作。

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

  1. 与集合底层数据结构有关。如,使用哈希表实现集合,要比链表实现的集合访问效率高;
  2. 和操作本身特点有关,比如读写一个元素比读写所有元素效率高。

        到这里你明白了为什么Redis快了吗?什么时候进行Rehash?其他的底层数据结构不做过多介绍,欢迎留言提问讨论。

往期精彩内容推荐

透视Redis大key背后的I/O挑战-CSDN博客

揭秘Redis中AOF与RDB的协同作战,确保数据万无一失-CSDN博客

Redis持久化(AOF、RDB)用到的写时复制到底是什么_rdb写时复制如何实现的-CSDN博客

  • 20
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

超越不平凡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值