【Redis】为什么Redis单线程却能支撑高并发?Redis6.0之后为什么又引入多线程?

一、为什么Redis单线程却能支撑高并发?

Redis 和 memcached 有什么区别?Redis 的线程模型是什么?为什么 Redis 单线程却能支撑高并发?

  这个是问 Redis 的时候,最基本的问题吧,Redis 最基本的一个内部原理和特点,就是 Redis 实际上是个单线程工作模型,你要是这个都不知道,那后面玩儿 Redis 的时候,出了问题岂不是什么都不知道?

  还有可能面试官会问问你 Redis 和 memcached 的区别,但是 memcached 是早些年各大互联网公司常用的缓存方案,但是现在近几年基本都是 Redis ,没什么公司用 memcached 了。

Redis 和 memcached 有啥区别?

  • 存储方式:memcached 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。 Redis 有部分存在硬盘上,这样能保证数据的持久性。
  • 数据支持类型: memcached 对数据类型支持相对简单。 Redis 有复杂的数据类型。
  • 底层模型: 它们之间底层实现方式以及与客户端之间通信的应用协议不一样。 Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

Redis 相比 memcached 来说,拥有更多的数据结构,能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, Redis 会是不错的选择。

Redis 原生支持集群模式
  在 redis3.x 版本中,便能支持 cluster 模式,而 memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。

性能对比
  由于 Redis 只使用单核,而 memcached 可以使用多核,所以平均每一个核上 Redis 在存储小数据时比 memcached 性能更高。而在 100k 以上的数据中,memcached 性能要高于 redis,虽然 Redis 最近也在存储大数据的性能上进行优化,但是比起 memcached,还是稍有逊色。

redis 的线程模型

  Redis客户端对服务端的每次调用都经历了发送命令执行命令返回结果三个过程。其中执行命令阶段,由于Redis是单线程来处理命令的,所以每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。并且多个客户端发送的命令的执行顺序是不确定的。但是可以确定的是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。

  Redis 内部使用文件事件处理器(file event handler)这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。也就是说Redis 的单线程指的是执行 Redis 命令的核心模块是单线程的,而不是整个 Redis 实例就一个线程,Redis 其他模块还有各自模块的线程的,Redis 4.0 开始就有多线程的概念了,比如 Redis 通过多线程方式在后台删除对象、以及通过 Redis 模块实现的阻塞命令等。Redis 采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。

文件事件处理器的结构包含 4 个部分: 多个 socket、IO 多路复用程序、文件事件分派器、事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
  多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

来看客户端与 Redis 的一次通信过程:
在这里插入图片描述
  客户端 socket01 向 Redis 的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。

  假设此时客户端发送了一个 set key value 请求,此时 Redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。

  如果此时客户端准备好接收返回结果了,那么 Redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。

这样便完成了一次通信。

为啥 Redis 单线程模型也能效率这么高?总结起来就是以下几点:

  • 纯内存访问:数据存放在内存中,内存的响应时间大约是100纳秒,这是Redis每秒万亿级别访问的重要基础。
  • 非阻塞I/O:Redis采用epoll做为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接,读写,关闭都转换为了事件,不在I/O上浪费过多的时间。
  • 单线程避免了线程切换和竞态产生的消耗。

Redis采用单线程模型,每条命令执行如果占用大量时间,会造成其他线程阻塞,对于Redis这种高性能服务是致命的,所以Redis是面向高速执行的数据库。我们还可以来参考知乎上大佬们的讨论加深自己的理解:redis为什么是单线程?在多核处理器下对主存的访问真的比多线程更有效率?未来有可能改用多线程吗?

并发和并行通常被认为是不同的概念,有什么区别?
1、并发性I/O流,意味着能够让一个计算单元来处理来自多个客户端的流请求。
2、并行性,意味着服务器能够同时执行几个事情(具有多个计算单元),这是不同的。
例如,酒吧能够服务几个顾客,同时他只能一次准备一个饮料。所以他可以提供没有并行性的并发服务。
Redis虽然是单线程程序,但可以通过使用I / O多路复用同一个线程和事件循环在I / O级别上提供并发性。像Redis这样的高效存储引擎的瓶颈通常是网络,在CPU之上。因此,Redis原子性(隔离事件循环)在没有额外成本的情况下提供并发性(不需要进程、线程同步),无需支付同步开销。并行性有一个代价:现在硬件设备都是多核,当然多核速度肯定比单核效率高,但进程线程之间的同步是非常昂贵的。好比,人家redis只要一台服务器可以搞定的事情,你干嘛一定要让我使用多台服务器。它轻巧,可作为构建高效可扩展的服务器,何必纠结一定要装波音747的发动机(对应的设计比较复杂),redis设计有它自己架构的好处!官网也说了,要发挥多核CPU性能,可以通过在单机开多个Redis core实例来完善,一样实现分布式

二、Redis6.0之后为什么又引入多线程?

1、传统阻塞IO模型

  在看反应器模式前,这里有必要提一下传统阻塞IO模型的处理方式。
  在传统阻塞IO模型中,由一个独立的 Acceptor 线程来监听客户端的连接,每当有客户端请求过来时,它就会为客户端分配一个新的线程来进行处理。当同时有多个请求过来,服务端对应的就会分配相应数量的线程。这就会导致CPU频繁切换,浪费资源。
  有的连接请求过来不做任何事情,但服务端还会分配对应的线程,这样就会造成不必要的线程开销。这就好比去餐厅吃饭,拿着菜单看了半天发现真的贵,然后就走人了。这段时间等点菜的服务员就相当于一个对应的线程,要点菜可以看作一个连接请求。
在这里插入图片描述
  同时,每次建立连接后,当线程调用读写方法时,线程会被阻塞,直到有数据可读可写, 在此期间线程不能做其它事情。还是上边餐厅吃饭的例子,你出去转了一圈发现还是这家性价比最高。回到这家餐厅又拿着菜单看了半天,服务员也在旁边等你点完菜为止。这个过程中服务员什么也不能做,只能这么干等着,这个过程相当于阻塞。
在这里插入图片描述
  这样的方式,每来一个请求就要分配一个线程,并且还得阻塞地等线程处理完。有的请求还只是过来连接下,什么操作也不干,还得为它分配一个线程,对服务器资源要求那得多高啊。遇到高并发场景,不敢想象。对于连接数目比较小的的固定架构倒是可以考虑。

2、伪异步IO模型

  一种通过线程池优化的解决方案,采用线程池和任务队列的方式。这种被称作伪异步IO模型。
当有客户端接入时,将客户端的请求封装成一个 task 投递到后端线程池中来处理。线程池维护一个消息队列和多个活跃线程,对消息队列中的任务进行处理。
在这里插入图片描述
  这种解决方案,避免了为每个请求创建一个线程导致的线程资源耗尽问题。但是底层仍然是同步阻塞模型。如果线程池内的所有线程都阻塞了,那么对于更多请求就无法响应了。因此这种模式会限制最大连接数,并不能从根本上解决问题。
  继续用上边的餐厅来举例,餐厅老板在经营了一段时间后,顾客多了起来,原本店里的5个服务员一对一服务的话根本对付不过来。于是老板采用5个人线程池的方式。服务员服务完一个客人后立刻去服务另一个。
  这时问题出现了,有的客人点菜特别慢,服务员就得等待很长时间,直到客人点完为止。如果5个客人都点的特别慢的话,这5个服务员就得一直等下去,就会导致其余的顾客没有人服务的状态。这就是上边所说的线程池所有线程都被阻塞的情况。
那么这种问题该如何解决呢?别急, Reactor 模式就要出场了。

3、Reactor设计模式

  Reactor 模式的基本设计思想是基于I/O复用模型来实现的。
  这里说下I/O复用模型。和传统IO多线程阻塞不同,I/O复用模型中多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
  什么意思呢?餐厅老板也发现了顾客点餐慢的问题,于是他采用了一种大胆的方式,只留了一个服务员。当客人点餐的时候,这个服务员就去招待别的客人,客人点好餐后直接喊服务员来进行服务。这里的顾客和服务员可以分别看作多个连接和一个线程。服务员阻塞在一个顾客那里,当有别的顾客点好餐后,她就立刻去服务其他的顾客。
  了解了 reactor 的设计思想后,再来看下单 reactor 单线程的实现方案:
在这里插入图片描述
  Reactor 通过 I/O复用程序监控客户端请求事件,收到事件后通过任务分派器进行分发。

  • 针对建立连接请求事件,通过 Acceptor 处理,并建立对应的 handler 负责后续业务处理。
  • 针对非连接事件,Reactor 会调用对应的 handler 完成 read->业务处理->write 处理流程,并将结果返回给客户端。

整个过程都在一个线程里完成。
在这里插入图片描述
单线程时代
  Redis 是基于 Reactor 单线程模式来实现的。
  IO多路复用程序接收到用户的请求后,全部推送到一个队列里,交给文件分派器。对于后续的操作,和在 reactor 单线程实现方案里看到的一样,整个过程都在一个线程里完成,因此 Redis 被称为是单线程的操作。
在这里插入图片描述
对于单线程的 Redis 来说,基于内存,且命令操作时间复杂度低,因此读写速率是非常快的。
多线程时代
  Redis6 版本中引入了多线程。上边已经提到过 Redis 单线程处理有着很快的速度,那为什么还要引入多线程呢?单线程的瓶颈在什么地方?
  先来看第二个问题,在 Redis 中,单线程的性能瓶颈主要在网络IO操作上。也就是在读写网络 read/write 系统调用执行期间会占用大部分 CPU 时间。如果要对一些大的键值对进行删除操作的话,在短时间内是删不完的,那么对于单线程来说就会阻塞后边的操作。
  回想下上边讲得 Reactor 模式中单线程的处理方式。针对非连接事件,Reactor 会调用对应的 handler 完成 read->业务处理->write处理流程,也就是说这一步会造成性能上的瓶颈。

Redis 在设计上采用将网络数据读写和协议解析通过多线程的方式来处理,对于命令执行来说,仍然使用单线程操作。

文件事件处理器(file event handler)

  • Redis 基于 Reactor 模式开发了自己的网络事件处理器: 这个处理器被称为文件事件处理器(file event handler)
  • 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
  • 文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。

总结

Reactor模式

  • 传统阻塞IO模型客户端与服务端线程1:1分配,不利于进行扩展。
  • 伪异步IO模型采用线程池方式,但是底层仍然使用同步阻塞方式,限制了最大连接数。
  • Reactor 通过 I/O复用程序监控客户端请求事件,通过任务分派器进行分发。
    单线程时代
  • 基于 Reactor 单线程模式实现,通过IO多路复用程序接收到用户的请求后,全部推送到一个队列里,交给文件分派器进行处理。

多线程时代

  • 单线程性能瓶颈主要在网络IO上。
  • Redis 的多线程部分只是用来处理网络数据的读写和协议解析,对于命令执行来说,仍然使用单线程操作。之所以这么设计是不想 Redis 因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。

文章来源:
[1]、为什么Redis 是单线程却能支撑高并发?
[2]、Redis6.0之多线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

童话ing

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

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

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

打赏作者

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

抵扣说明:

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

余额充值