概览

单线程

为什么单线程的 Redis 能那么快?

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

为什么用单线程?

多线程编程模式面临的共享资源的 并发访问控制处理带来的性能下降

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

多路复用

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

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

Reactor

Reactor 模型是基于事件驱动的并发处理模式,它在处理网络 I/O 时充分利用了操作系统提供的多路复用技术。

单线程Reactor模式流程

消息处理流程:

  1. Reactor对象通过select/poll/epoll等IO多路复用监控连接事件,收到事件后通过dispatcher事件分发器进行转发。

  2. 如果是连接建立的事件,则由acceptor接受连接,并创建Handler处理后续事件。

  3. 如果不是建立连接事件,则Reactor会分发调用Handler来响应。

  4. Handler会完成read->解析->执行->send的完整业务流程。

优点:

  • 单线程运行,串行操作,不需要加锁,逻辑简单。

缺点:

  • 仅用一个线程处理请求,对于多核资源机器来说是有点浪费的。

  • 当处理读写任务的线程负载比较重,将会阻塞后续的事件处理,导致整体延迟变大。

应用:

  • Redis网络模型。(6.0版本以前)

Master-Worker Reactor模型

比起单线程模型,它是将Reactor分成两部分:

  • mainReactor 负责监听server socket,用来处理网络IO连接建立操作,将建立的socketChannel指定注册给subReactor。 (只负责监听)

  • subReactor 主要做和建立起来的socket做数据交互和事件业务处理操作。通常,subReactor个数上可与CPU个数等同。一般是多个,这样的话,就可以充分利用多核的优势。 (负责IO读写和命令的执行)

区别于单线程Reactor模式,这种模式由 mainReactor 负责接收新连接并分发给 subReactors 去独立处理,subReactors 执行从网络IO读取数据、解码、计算、编码、回写客户端数据。

优点:

  • 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;

  • 可扩展性,可以方便地通过增加Reactor实例个数来充分利用CPU资源;

缺点:

  • 如果多个线程可能操作同一份数据,就涉及到底层数据同步的问题,则必然会引入某些同步机制,比如锁。增加了代码复杂度,同时增加了同步机制的开销。

应用:

  • Nginx, Netty, Swoole, Memcached就是使用的这个模型

Redis 6.0 网络模型

Redis 6.0版本之后,Redis 正式在核心网络模型中引入了多线程,也就是所谓的 I/O threading,至此 Redis 真正拥有了多线程模型。 但是Redis的多线程模型却并非标准的Master-Worker Reactor模型,他的多线程 只负责IO读写,不负责具体的执行。

将主线程 IO 读写任务拆分出来给一组独立的线程处理,使得多个 socket 读写可以并行化,但是 Redis 命令还是主线程串行执行。

设计原因

  1. 前面提到 Redis 最初选择单线程网络模型的理由是:CPU 通常不会成为性能瓶颈,瓶颈往往是内存和网络,因此单线程足够了。那么为什么现在 Redis 又要引入多线程呢?很简单,就是 Redis 的网络 I/O 瓶颈已经越来越明显了。所以这个多线程是为了解决IO的瓶颈的。

  2. 如果多线程包括了IO读写,解析和执行的整个过程,那么多线程需要面临线程安全的问题,Redis 6.0版本之前是没有考虑线程安全的,如果使用多线程来处理命令的执行,需要大量的改动来保证多线程的安全机制,实现更复杂。为了避免了不必要的上下文切换和竞争条件,多线程导致的切换而消耗 CPU,也不用考虑各种锁的问题,就让执行这一步只使用主线程。


读取指令过程

image-tkks.png

首先,主线程负责接收建立连接请求。当有客户端请求和实例建立 Socket 连接时,主线程 会创建和客户端的连接,并把 Socket 放入全局等待队列中。紧接着,主线程通过轮询方法 把 Socket 连接分配给 IO 线程。

主线程一旦把 Socket 分配给 IO 线程,就会进入阻塞状态,等待 IO 线程完成客户端请求 读取和解析。因为有多个 IO 线程在并行处理,所以,这个过程很快就可以完成。

等到 IO 线程解析完请求,主线程还是会以单线程的方式执行这些命令操作。

写回结果过程

当主线程执行完请求操作后,会把需要返回的结果写入缓冲区,然后,主线程会阻塞等待 IO 线程把这些结果回写到 Socket 中,并返回给客户端。

和 IO 线程读取和解析请求一样,IO 线程回写 Socket 时,也是有多个线程在并发执行, 所以回写 Socket 的速度也很快。等到 IO 线程回写 Socket 完毕,主线程会清空全局队 列,等待客户端的后续请求。

使用

  1. 设置 io-thread-do-reads 配置项为 yes,表示启用多线程。

io-threads-do-reads yes

  1. 设置线程个数。一般来说,线程个数要小于 Redis 实例所在机器的 CPU 核个数,例 如,对于一个 8 核的机器来说,Redis 官方建议配置 6 个 IO 线程。

io-threads 6

如果你在实际应用中,发现 Redis 实例的 CPU 开销不大,吞吐量却没有提升,可以考虑使 用 Redis 6.0 的多线程机制,加速网络处理,进而提升实例的吞吐量。