zoukankan      html  css  js  c++  java
  • 为什么单线程Redis能那么快?

    Redis单线程

      我们通常说,Redis是单线程,主要是指Redis的网络I/O和键值读写的是由一个线程来完成的。其他数据持久化、集群数据同步、异步删除等,其实是由额外线程来完成的。

      所以,严格来说,Redis 并不是单线程,但是我们一般把 Redis 称为单线程高性能。接下来,会把 Redis 称为单线程模式。我们会想到:“为什么用单线程?为什么单线程能这么快?

      要弄明白这个问题,我们就要深入地学习下 Redis 的单线程设计机制以及多路复用机制。之后你在调优 Redis 性能时,也能更有针对性地避免会导致 Redis 单线程阻塞的操作,例如执行复杂度高的命令。

    Redis 为什么用单线程?

      要更好地理解 Redis 为什么用单线程,我们就要先了解多线程的开销。

    多线程的开销

      在日常写程序时,我们通常回听到使用多线程会增加系统的吞吐率,或者可以增加系统的扩展性。确实对于一个多线程的系统,在合理的分配系统资源的情况下,可以增加系统处理请求操作的资源实体,进而提升系统能够同时处理的请求数,即吞吐率。以下左图是我们期望的效果。

      但是,在通常情况下,在我们采用多线程后,如果没有精细的系统设计,实际得到的效果却不尽人意。在开始增加线程数的时候,系统的吞吐率会提升,在进一步增加线程数的时候,系统的吞吐率提升就缓慢了,有时甚至出现下降的情况。

       为什么会出现这个问题,因为在多线程的系统中,当出现多个线程同时需要访问共享资源的时候,比如一个共享的数据结构。当有多个线程需要修改这个共享资源时,为了保证共享资源的正确性,就需要额外的机制去保证,而这个额外的机制就会带来额外的系统开销。多线程会面临共享资源的并发访问控制问题

    单线程 Redis 为什么那么快?

      通常来说,单线程的处理能力要比多线程差得多,但是Redis为什么能够使用单线程达到每秒数十万级别的处理能力呢?其实,时Redis多方面设计的一个综合结果。

      一方面,Redis的操作都在内存中完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。另一方面,Redis使用了多路复用机制,使其在网络的IO的时候能够并发处理大量的客户端请求,实现高吞吐率。我们来学习下多路复用机制。

      首先,我们要弄明白网络操作的基本 IO 模型和潜在的阻塞点。毕竟,Redis 采用单线程进行 IO,如果线程被阻塞了,就无法进行多路复用了。

    基本 IO 模型与阻塞点

      这里以具有网络框架的简单键值库为例说明。

      以Get请求为例,为了处理一个Get请求,需要先监听客户端请求(bind/listen),建立客户端连接(accept),解析客户端请求(parse),根据请求模型获取键值数据(get),最后给客户端返回结果,即向socket中写回数据(send)。

      下图展示了这一过程,其中,bind/listen、accept、parse和send都属于网络I/O处理,而get数据键值数据操作。既然Redis是单线程,最基本的一种实现是在一个线程中依次执行上面说的这些操作。

      但是,在这里的网络 IO 操作中,有潜在的阻塞点,分别是 accept() 和 recv()。当 Redis 监听到一个客户端有连接请求,但一直未能成功建立起连接时,会阻塞在 accept() 函数这里,导致其他客户端无法和 Redis 建立连接。类似的,当 Redis 通过 recv() 从一个客户端读取数据时,如果数据一直没有到达,Redis 也会一直阻塞在 recv()。

      这就导致 Redis 整个线程阻塞,无法处理其他客户端请求,效率很低。不过,幸运的是,socket 网络模型本身支持非阻塞模式。

    非阻塞模式

       Socket 网络模型的非阻塞模式设置,主要体现在三个关键的函数调用上,如果想要使用 socket 非阻塞模式,就必须要了解这三个函数的调用返回类型和设置模式。接下来,我们就重点学习下它们。

      在Socket模型中,不同的操作回调用后回返回不同的套接字类型。socket()方法会返回主动套接字,然后返回listen()方法,将主动套接字转化为监听套接字,此时,可以监听来自客户端的连接请求。最后,调用accept()方法接收到达的客户端连接,并返回已连接套接字。

      针对监听套接字,我们可以设置非阻塞模式:当Redis调用accpet()但一直未有连接请求到达时,Redis线程可以返回处理其他操作,而不用一直等待。但是,需要我们注意的是,调用accept()时,已经存在监听套接字了。

      虽然Redis线程可以不用继续等待,但是总得有机制继续在监听套接字上等待后续连接请求,并在有请求得时候通知Redis。

      类似的,我们也可以针对已连接套接字设置非阻塞模式:Redis调用recv()后,如果已连接套接字上一直没有数据到达,Redis线程同样可以返回处理其他操作。我们也需要有机制继续监听改已连接套接字,并在有数据到达时通知Redis。

      这样才能保证Redis线程,既不会像基本IO模型中一直在阻塞点等待,也不会导致Redis无法处理实际到达得连接请求或数据,至此,Linux中得IO多路复用机制就要登场了。

    基于多路复用的高性能 I/O 模型

      Linux中得IO多路复用机制是指一个线程处理多个IO流,就是我们经常听到得select/epoll机制。简单来说,在Redis只允许单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给Redis线程处理,这就实现了一个Redis线程处理多个IO流的效果。

      下图就是基于多路复用的 Redis IO 模型。图中的多个 FD 就是刚才所说的多个套接字。Redis 网络框架调用 epoll 机制,让内核监听这些套接字。此时,Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。

      为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数

      那么,回调机制是怎么工作的呢?其实,select/epoll一旦检测到FD上有请求时,就会触发相应的事件。

      这些事件会被放入到一个事件队列,Redis单线程对该事件队列不断进行处理。这样依赖,Redis无需一直轮询是否有请求实际发生,这就可以避免造成CPU资源浪费。同时,Redis在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为Redis一直在对事件队列进行处理,所以能及时相应客户端请求,提升Redis的响应性能。

      为了方便理解,以连接请求和读数据请求为例,解释:

      这两个请求分别对应 Accept 事件和 Read 事件,Redis 分别对这两个事件注册 accept 和 get 回调函数。当 Linux 内核监听到有连接请求或读数据请求时,就会触发 Accept 事件和 Read 事件,此时,内核就会回调 Redis 相应的 accept 和 get 函数进行处理。

      

    小结

    今天,本次整理了 Redis 线程的三个问题:“Redis 真的只有单线程吗?”“为什么用单线程?”“单线程为什么这么快?

      1、Redis的单线程是指它对网络IO和数据读写的操作采用了一个线程。

      2、采用单线程的核心原因就是避免多线程开发的并发控制问题。

      3、单线程的Redis也能获得高性能,跟多路复用的 IO 模型密切相关,因为这避免了 accept() 和 send()/recv() 潜在的网络 IO 操作阻塞点。

  • 相关阅读:
    Linux file命令详解
    Linux stat命令详解
    Linux cut命令详解
    Linux tr命令详解
    Linux grep/egrep命令详解
    Linux awk命令详解
    Linux xargs命令详解
    MVC设计模式
    qt博客
    android
  • 原文地址:https://www.cnblogs.com/wushaoyu/p/15102224.html
Copyright © 2011-2022 走看看