当前位置:   article > 正文

Redis(四):线程模型_redis线程模型

redis线程模型

前言

上一篇介绍了 Redis 定义的九大数据类型。这节开始介绍 Redis 中的线程模型:单线程模型和多线程模型。

单线程模型

Redis 的单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的。但其他部分功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。

Redis 采用单线程(网络 I/O 和执行命令)还那么快,有如下几个原因:

  • Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此性能瓶颈并非 CPU;
  • Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题;
  • Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

CPU 并不是制约 Redis 性能表现的瓶颈所在,更多情况下是受到内存大小和网络I/O的限制,所以 Redis 使用单线程并没有什么问题。但是如果你想要使用服务器的多核 CPU,可以在一台服务器上启动多个节点或者采用切片集群的方式。

Redis 为什么要使用单线程模型?多线程不是可以提高性能吗?

多线程模式会面临执行顺序的不确定、共享资源的并发访问控制等问题,同时可能存在线程切换、加锁解锁和死锁的问题,需要额外的性能开销。而 Redis 主要的工作都是键值对的读写,所以单线程反而性能更高。且 Redis 的性能瓶颈主要在磁盘 IO 和网络 IO 方面,而多线程主要用于解决 CPU 的性能瓶颈。

后台线程

上文提到 Redis 的单线程是指网络 IO 和键值对读写由主线程完成,但是还有其他工作是交给子线程/进程来完成的。

Redis 在启动的时候,会启动后台线程(BIO):

  • Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
  • Redis 在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。例如执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。

当我们要删除大量数据的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,会导致 Redis 主线程卡顿,应该使用 unlink 命令来异步删除。

后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者不停轮询这个队列,拿出任务就去执行对应的方法即可。

在这里插入图片描述

关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列:

  • BIO_CLOSE_FILE,关闭文件任务队列:当队列有任务后,后台线程会调用 close(fd) ,将文件关闭;
  • BIO_AOF_FSYNC,AOF刷盘任务队列:主线程会把 AOF 写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用 fsync(fd),将 AOF 文件刷盘;
  • BIO_LAZY_FREE,lazy free 任务队列:当队列有任务后,后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象;
网络模式

网络模式大致可以分为 Reactor 和 Proactor,Reactor 是非阻塞同步网络模式,而 Proactor 是异步网络模式

Redis 使用的是 Reactor 模式,所以先简单介绍一下 Reactor。

Reactor 模式主要由 Reactor 和处理资源池这两个核心部分组成:

  • Reactor 负责监听和分发事件,事件类型包含连接事件、读写事件;
  • 处理资源池负责处理事件,如 read -> 业务逻辑 -> send;

Reactor 模式是灵活多变的,可以应对不同的业务场景,Reactor 的数量可以是单个或多个,处理资源的进程/线程也可以是单个或多个,所以就有了这四种方案:

  • 单 Reactor 单进程 / 线程;
  • 单 Reactor 多进程 / 线程;
  • 多 Reactor 单进程 / 线程;
  • 多 Reactor 多进程 / 线程;

其中,「多 Reactor 单进程 / 线程」实现方案相比「单 Reactor 单进程 / 线程」方案,不仅复杂而且也没有性能优势,因此实际中并没有应用。

Redis 的单线程模型使用的是「单 Reactor 单进程」模式。

在这里插入图片描述

进程里有 Reactor、Acceptor、Handler 这三个对象:

  • Reactor 对象的作用是监听和分发事件;
  • Acceptor 对象的作用是获取连接;
  • Handler 对象的作用是处理业务;

「单 Reactor 单进程」方案:

  • Reactor 对象通过 epoll(IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给哪个对象,还要看收到的事件类型;
  • 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
  • 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
  • Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。

单 Reactor 单进程的方案因为全部工作都在同一个进程内完成,所以实现起来比较简单,不需要考虑进程间通信,也不用担心多进程竞争。

但是,这种方案存在 2 个缺点:

  • 只有一个进程,无法充分利用 多核 CPU 的性能
  • Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟

所以,单 Reactor 单进程的方案不适用计算密集型的场景,只适用于业务处理非常快速的场景

网络I/O

在单线程模型下,IO 多路复用允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。

为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。

Redis 使用的 IO 多路复用技术主要有:基于 Linux 系统下的 select 和 epoll 实现,基于 FreeBSD 的 kqueue 实现,基于 Solaris 的 evport 实现。

每个 IO 多路复用函数库在 Redis 源码中都对应一个单独的文件,比如ae_select.cae_epoll.cae_kqueue.c等。Redis 会根据不同的操作系统,按照不同的优先级选择多路复用技术。

Redis 6.0 版本之前的单线模式如下图:

在这里插入图片描述

可以看到网络 I/O 和命令处理都是单线程。 Redis 初始化的时候,会做下面这几件事情:

  1. 调用 epoll_create() 创建一个 epoll 对象和调用 socket() 创建一个服务端 socket;
  2. 调用 bind() 绑定端口和调用 listen() 监听该 socket;
  3. 将调用 epoll_ctl() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数。

初始化完后,主线程就进入到一个事件循环函数,主要会做以下事情:

  • 首先,先调用处理发送队列函数,检查发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
  • 接着,调用 epoll_wait 函数等待事件的到来:
    • 如果是连接事件到来,则会调用连接事件处理函数,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数;
    • 如果是读事件到来,则会调用读事件处理函数,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送;
    • 如果是写事件到来,则会调用写事件处理函数,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。

多线程模型

随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度。Reids6.0 提出了多线程模型,可以采用多个 IO 线程来处理网络请求(默认关闭),提高网络请求处理的并行度。

仍然使用单线程执行读写命令操作,是因为命令操作往往不会有性能瓶颈,单线程处理可以避免额外的互斥操作,性能更高。

网络模式

Redis 6.0 以后的多线程模型采用的是「单 Reactor 多线程」方案。

在这里插入图片描述

「单 Reactor 多线程」方案:

  • Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
  • 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
  • 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;

上面的三个步骤和单 Reactor 单线程方案是一样的,接下来的步骤就开始不一样了:

  • Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给子线程里的 Processor 对象进行业务处理;
  • 子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;

单 Reator 多线程的方案优势在于能够充分利用多核 CPU 的能,但是引入多线程,自然就带来了多线程竞争资源的问题。要避免多线程由于竞争共享资源而导致数据错乱的问题,就需要在操作共享资源前加上互斥锁,以保证任意时间里只有一个线程在操作共享资源,待该线程操作完释放互斥锁后,其他线程才有机会操作共享数据。

网络I/O

在这里插入图片描述

主要流程:

  1. 客户端建立 Socket 连接,并分配处理线程:主线程负责接收建立连接请求。当有客户端请求和实例建立 Socket 连接时,主线程会创建和客户端的连接,并把 Socket 放入全局等待队列中。
  2. 分配 IO 线程:主线程通过轮询方法把 Socket 连接分配给 IO 线程。
  3. IO 线程读取并解析请求:主线程一旦把 Socket 分配给 IO 线程,就会进入阻塞状态,等待 IO 线程完成客户端请求读取和解析。因为有多个 IO 线程在并行处理,所以这个过程很快就可以完成。
  4. 主线程执行请求操作:等到 IO 线程解析完请求,主线程还是会以单线程的方式执行这些命令操作。
  5. IO 线程回写 Socket :当主线程执行完请求操作后,会把需要返回的结果写入缓冲区,然后,主线程会阻塞等待 IO 线程把这些结果回写到 Socket 中,并返回给客户端。
  6. 主线程清空全局队列:等到 IO 线程回写 Socket 完毕,主线程会清空全局队列,等待客户端的后续请求。

线程数是不是越多越好?

因为 Redis 的多线程模型采用的是固定数量的线程池,线程数是在启动 Redis 时就固定的,不能动态调整。如果线程数过多,会导致线程之间频繁地切换上下文,这样会浪费CPU资源,降低 Redis 的性能。另外,线程数过多还会占用过多的内存资源,可能会导致系统崩溃。

官方建议线程数设置为小于 CPU 核数,且不要超过 8 个。

最后

本文介绍了 Redis 的单线程模型和多线程模型,单线程模型是指网络 IO 和读写键值对由主线程这一个线程来完成,而其他操作可以交由子线程完成。多线程则是在单线程的基础上,采用多个线程来处理网络请求。下一节将介绍 Redis 之间是如何进行通信的。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/157346
推荐阅读
相关标签
  

闽ICP备14008679号