Redis为什么这么快?在除了它是内存型数据库外,它的数据结构,IO模型也是它这么快速的重要原因。我们都知道Redis是单线程模型,那么Redis是如何利用单线程快速的处理高并发业务的呢?带着疑问来看下边的内容更容易理解。

Redis为什么使用单线程?

了解多线程的开销
  1. 在使用多线程中,如果合理分配资源可以增加系统中处理请求操作的实体,进而能够提升同时处理的请求数,即吞吐率。但是如图所示进一步增加线程时,系统吞吐率就增长延迟了,有时甚至还会出现下降的情况。

Multi-threaded processor

参考文章

  1. 系统中通常会存在被多线程同时访问的共享资源,比如一个共享的数据结构。当有多个线程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,就会带来额外的开销。

    Redis 有 List 的数据类型,并提供出队(LPOP)和入队(LPUSH)操作。假设 Redis 采用多线程设计,如下图所示,现在有两个线程 A 和 B,线程 A 对一个 List 做 LPUSH 操作,并对队列长度加 1。同时,线程 B 对该 List 执行 LPOP 操作,并对队列长度减 1。为了保证队列长度的正确性,Redis 需要让线程 A 和 B 的 LPUSH 和 LPOP 串行执行,这样一来,Redis 可以无误地记录它们对 List 长度的修改。否则,我们可能就会得到错误的长度结果。这就是多线程编程模式面临的共享资源的并发访问控制问题。

  2. 并发访问控制一直是多线程开发中的一个难点问题,如果没有精细的设计,比如说,只是简单地采用一个粗粒度互斥锁,就会出现不理想的结果:即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。

  3. 采用多线程开发一般会引入同步原语来保护共享资源的并发访问,这也会降低系统代码的易调试性和可维护性。为了避免这些问题,Redis 直接采用了单线程模式。

单线程Redis为什么快?

  1. 大部分属于内存操作
  2. 高效数据库
  3. IO多路复用

阻塞式I/O模型与阻塞点

阻塞式I/O处理一个GET请求流程:

  1. 监听客户端请求(bind/listen)
  2. 和客户端建立连接(accept)阻塞点1
  3. 从socket中读取请求(recv)阻塞点2
  4. 解析客户端发送请求(parse)
  5. 根据请求读取键值对数据(get)
  6. 向socket中写数据,最后给客户端返回结果(send)
img

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

非阻塞式I/O

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

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

img
  1. 针对监听套接字,我们可以设置非阻塞模式:当 Redis 调用 accept() 但一直未有连接请求到达时,Redis 线程可以返回处理其他操作,而不用一直等待。但是,你要注意的是,调用 accept() 时,已经存在监听套接字了。
  2. 针对已连接套接字设置非阻塞模式:Redis 调用 recv() 后,如果已连接套接字上一直没有数据到达,Redis 线程同样可以返回处理其他操作。我们也需要有机制继续监听该已连接套接字,并在有数据达到时通知 Redis。
  3. 这样才能保证 Redis 线程,既不会像基本 I/O 模型中一直在阻塞点等待,也不会导致 Redis 无法处理实际到达的连接请求或数据。

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

Linux中的I/O多路复用就是一个线程处理多个I/O流,就是我们经常说的select/epoll(理解IO模型)机制。

Redis单线程运行下该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会监听这些套接字上的连接请求或者数据请求。一旦有请求到达,就会交给redis线程处理,这样就实现了一个Redis线程处理多个I/O流的效果

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

img
  1. select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。
  2. select/epoll 一旦监测到 FD 上有请求到达时,就会触发相应的事件。
  3. 这些事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理。这样一来,Redis 无需一直轮询是否有请求实际发生,这就可以避免造成 CPU 资源浪费。同时,Redis 在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为 Redis 一直在对事件队列进行处理,所以能及时响应客户端请求,提升 Redis 的响应性能。
Accept、Read处理流程

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