【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 内部使用文件事件处理器(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 单线程模型也能效率这么高?总结起来就是以下几点:

  • 纯内存操作
  • 核心是基于非阻塞的 IO 多路复用机制
  • 单线程反而避免了多线程的频繁上下文切换问题

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

总结

Reactor模式

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

多线程时代

  • 单线程性能瓶颈主要在网络IO上。
  • 将网络数据读写和协议解析通过多线程的方式来处理 ,对于命令执行来说,仍然使用单线程操作。

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

猜你喜欢

转载自blog.csdn.net/dl962454/article/details/115218011