vlambda博客
学习文章列表

和Redis的线程模型做个“了结”

和Redis的线程模型做个“了结”

前两天面试遇到过关于Redis的单线程模型的问题,这一次针对这个知识点做一个系统性的总结⑧。


章节目录:

  1. 线程模型的组成;

  2. Redis为啥是单线程?

  3. 单线程模型的优劣势?

  4. 为啥Redis是单线程却支持高并发?




和Redis的线程模型做个“了结”
1. 线程模型的组成

Redis基于Reactor模式(每一个网络连接其实都对应一个文件描述符)开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。

它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
  • 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。

  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

  • 虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。



和Redis的线程模型做个“了结”
和Redis的线程模型做个“了结”
2. 为什么是单线程?
  • Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。单线程容易实现。

  • 性能指标

    redis的性能,基于内存的,普通笔记本轻松处理每秒几十万的请求。

  • 详细原因

    1. 不需要各种锁的性能消耗  

      Redis的数据结构并不全是简单的Key-Value,还有list,hash等复杂的结构,这些结构有可能会进行很细粒度的操作,比如在很长的列表后面添加一个元素,在hash当中添加或者删除一个对象。这些操作可能就需要加非常多的锁,导致的结果是同步开销大大增加。总之,在单线程的情况下,就不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。

    2. 单线程多进程集群方案  

      单线程的威力实际上非常强大,每核心效率也非常高,多线程自然是可以比单线程有更高的性能上限,但是在今天的计算环境中,即使是单机多线程的上限也往往不能满足需要了,需要进一步摸索的是多服务器集群化的方案,这些方案中多线程的技术照样是用不上的。所以单线程、多进程的集群不失为一个时髦的解决方案。

    3. CPU消耗  

      采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU。  但是如果CPU成为Redis瓶颈,或者不想让服务器其他CUP核闲置,那怎么办?  可以考虑多起几个Redis进程,Redis是key-value数据库,不是关系数据库,数据之间没有约束。只要客户端分清哪些key放在哪个Redis进程上就可以了。


和Redis的线程模型做个“了结”
和Redis的线程模型做个“了结”
3. 单线程的优劣势
  1. 单进程单线程优势

    • 代码更清晰,处理逻辑更简单;

    • 不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗

    • 不存在多进程或者多线程导致的切换而消耗CPU 

  2. 单进程单线程弊端

    • CPU不是Redis的瓶颈,无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善;


和Redis的线程模型做个“了结”
4. 为什么单线程却可支持高并发


  1. 基于纯内存的操作,读写速度较高。

  2. 核心是基于非阻塞的IO多路复用机制。

  3. 单线程反而避免了多线程的频繁的上下文的切换问题。


这里再着重介绍一下IO多路复用技术:

  • redis 采用网络IO多路复用技术来保证在多连接的时候,系统的高吞吐量。

  • 多路—指的是多个socket连接;复用—指的是复用一个线程。

  • 多路复用主要有三种技术:selectpoll,epoll。

    • 其中,select函数原理:

      • 该方法的能够同时监控多个文件描述符的可读可写情况,当其中的某些文件描述符可读或者可写时,select 方法就会返回可读以及可写的文件描述符个数。

      • 看一下 select 函数使用的大致流程:

        int fd = /* file descriptor */

        fd_set rfds;
        FD_ZERO(&rfds);
        FD_SET(fd, &rfds)

        for ( ; ; ) {
            select(fd+1, &rfds, NULL, NULL, NULL);
            if (FD_ISSET(fd, &rfds)) {
                /* file descriptor `fd` becomes readable */
            }
        }
        • 初始化一个可读的 fd_set 集合,保存需要监控可读性的 FD;

        • 使用 FD_SET 将 fd 加入 rfds;

        • 调用 select 方法监控 rfds 中的 FD 是否可读;

        • 当 select 返回时,检查 FD 的状态并完成对应的操作。

  • 这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路I/O复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

    • 传统的阻塞 I/O 模型

      • 当使用 read 或者 write 对某一个文件描述符(File Descriptor 以下简称 FD)进行读写时,如果当前 FD 不可读或不可写,整个 Redis 服务就不会对其它的操作作出响应,导致整个服务不可用。

      • 阻塞模型虽然开发中非常常见也非常易于理解,但是由于它会影响其他 FD 对应的服务,所以在需要处理多个客户端任务的时候,往往都不会使用阻塞模型。

检讨一下,最近好久没刷题了,因为忙着面试和技术栈的查漏补缺,不过应该坚持每天早晚复习几题的,没好意思和可达鸭说,怕被挨讲。。