# 【NO.323】Redis6.0多线程模型总结

## **1. Redis6.0之前的版本真的是单线程吗？**

Redis在处理客户端的请求时，包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理，这就是所谓的“单线程”。但如果严格来讲从Redis4.0之后并不是单线程，除了主线程外，它也有后台线程在处理一些较为缓慢的操作，例如清理脏数据、无用连接的释放、大 key 的删除等等。其中执行命令阶段，由于 Redis 是单线程来处理命令的，所有每一条到达服务端的命令不会立刻执行，所有的命令都会进入一个 Socket 队列中，当 socket 可读则交给单线程事件分发器逐个被执行。如下图所示：

![img](https://pic4.zhimg.com/80/v2-b995dd6422b49e6fb10c522f12085aa3_720w.webp)

## **2. Redis6.0之前为什么一直不使用多线程？**

官方曾做过类似问题的回复：使用Redis时，几乎不存在CPU成为瓶颈的情况， Redis主要受限于内存和网络。例如在一个普通的Linux系统上，Redis通过使用pipelining每秒可以处理100万个请求，所以如果应用程序主要使用O(N)或O(log(N))的命令，它几乎不会占用太多CPU。

使用了单线程后，可维护性高。多线程模型虽然在某些方面表现优异，但是它却引入了程序执行顺序的不确定性，带来了并发读写的一系列问题，增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis通过AE事件模型以及IO多路复用等技术，处理性能非常高，因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低，Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。

## **3.Redis6.0为什么要引入多线程呢？**

Redis将所有数据放在内存中，内存的响应时长大约为100纳秒，对于小数据包，Redis服务器可以处理80,000到100,000 QPS，这也是Redis处理的极限了，对于80%的公司来说，单线程的Redis已经足够使用了。

但随着越来越复杂的业务场景，有些公司动不动就上亿的交易量，因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器，但该方案有非常大的缺点，例如要管理的Redis服务器太多，维护代价大；某些适用于单个Redis服务器的命令不适用于数据分区；数据分区无法解决热点读/写问题；数据偏斜，重新分配和放大/缩小变得更加复杂等等。

从Redis自身角度来说，因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间，瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向:

- 提高网络 IO 性能，典型的实现比如使用 DPDK 来替代内核网络栈的方式
- 使用多线程充分利用多核，典型的实现比如 Memcached。

协议栈优化的这种方式跟 Redis 关系不大，支持多线程是一种最有效最便捷的操作方式。所以总结起来，redis支持多线程主要就是两个原因：

- 可以充分利用服务器 CPU 资源，目前主线程只能利用一个核
- 多线程任务可以分摊 Redis 同步 IO 读写负荷

## **4.Redis6.0默认是否开启了多线程？**

Redis6.0的多线程默认是禁用的，只使用主线程。如需开启需要修改redis.conf配置文件：io-threads-do-reads yes

![img](https://pic3.zhimg.com/80/v2-b04eb451c09fd913bdc91cb77ddb9c9a_720w.webp)

## **5.Redis6.0多线程开启时，线程数如何设置？**

开启多线程后，还需要设置线程数，否则是不生效的。同样修改redis.conf配置文件。关于线程数的设置，官方有一个建议：4 核的机器建议设置为 2 或 3 个线程，8核的建议设置为 6 个线程，线程数一定要小于机器核数。线程数并不是越大越好，官方认为超过了 8 个基本就没什么意义了。

![img](https://pic4.zhimg.com/80/v2-111dd2a9e8ff54c2075bcf9cffc9616b_720w.webp)



## **6.Redis6.0多线程的实现机制？**

(1).流程如下：

1. 主线程获取 socket 放入等待列表
2. 将 socket 分配给各个 IO 线程（并不会等列表满）
3. 主线程阻塞等待 IO 线程（多线程）读取 socket 完毕
4. 主线程执行命令 - 单线程（如果命令没有接收完毕，会等 IO 下次继续）
5. 主线程阻塞等待 IO 线程（多线程）将数据回写 socket 完毕（一次没写完，会等下次再写）
6. 解除绑定，清空等待队列

(2).特点如下：

- IO 线程要么同时在读 socket，要么同时在写，不会同时读或写
- IO 线程只负责读写 socket 解析命令，不负责命令处理（主线程串行执行命令）
- IO 线程数可自行配置

![img](https://pic2.zhimg.com/80/v2-ef87bcc16bc8f85b64d7521d9eae3445_720w.webp)

流程简述如下：

- 1、主线程负责接收建立连接请求，获取 socket 放入全局等待读处理队列
- 2、主线程处理完读事件之后，通过 RR(Round Robin) 将这些连接分配给这些 IO 线程
- 3、主线程阻塞等待 IO 线程读取 socket 完毕
- 4、主线程通过单线程的方式执行请求命令，请求数据读取并解析完成，但并不执行
- 5、主线程阻塞等待 IO 线程将数据回写 socket 完毕
- 6、解除绑定，清空等待队列

![img](https://pic2.zhimg.com/80/v2-694b6077316b20e3f22cceda6a887eed_720w.webp)

该设计有如下特点：

- IO 线程要么同时在读 socket，要么同时在写，不会同时读或写
- IO 线程只负责读写 socket 解析命令，不负责命令处理

## **7.开启多线程后，是否会存在线程并发安全问题？**

从上面的实现机制可以看出，Redis的多线程部分只是用来处理网络数据的读写和协议解析，执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务，LPUSH/LPOP 等等的并发及线程安全问题。

## **8.redis单线程模型（6.0之前）**

Redis客户端对服务端的每次调用都经历了发送命令，执行命令，返回结果三个过程。其中执行命令阶段，由于Redis是单线程来处理命令的，所有每一条到达服务端的命令不会立刻执行，所有的命令都会进入一个队列中，然后逐个被执行。并且多个客户端发送的命令的执行顺序是不确定的。但是可以确定的是不会有两条命令被同时执行，不会产生并发问题，这就是Redis的单线程基本模型。

![img](https://pic4.zhimg.com/80/v2-a9771885d162d9b237a2e555d5161663_720w.webp)

PS：可以修改redis的最大链接数，默认为10000，如下图，如果要修改的话，直接修改配置文件重点maxclients即可。

![img](https://pic1.zhimg.com/80/v2-ca1a6dc988fae6d3d505331404166cb0_720w.webp)

### **8.1 什么是非阻塞IO？**

非阻塞 IO 在 Socket 对象上提供了一个选项Non_Blocking ，当这个选项打开时，读写方法不会阻塞，而是能读多少读多少，能写多少写多少。

能读多少取决于内核为 Socket 分配的读缓冲区的大小，能写多少取决于内核为 Socket 分配的写缓冲区的剩余空间大小。读方法和写方法都会通过返回值来告知程序实际读写了多少字节数据。

有了非阻塞 IO 意味着线程在读写 IO 时可以不必再阻塞了，读写可以瞬间完成然后线程可以继续干别的事了。

补充阻塞IO概念：

当我们调用 Scoket 的读写方法，默认它们是阻塞的。

read() 方法要传递进去一个参数 n，表示读取这么多字节后再返回，如果没有读够 n 字节线程就会阻塞，直到新的数据到来或者连接关闭了， read 方法才可以返回，线程才能继续处理。

write() 方法会首先把数据写到系统内核为 Scoket 分配的写缓冲区中，当写缓存区满溢，即写缓存区中的数据还没有写入到磁盘，就有新的数据要写道写缓存区时，write() 方法就会阻塞，直到写缓存区中有空闲空间。

### **8.2. 什么是IO多路复用？**

背景：

非阻塞 IO 有个问题，那就是单个线程要处理多个读写请求，处理某个客户端的的读数据的请求，结果读了一部分就返回了，线程如何知道什么时候才应该继续读数据。处理写请求的时候，如果缓冲区满了，写不完，剩下的数据何时才应该继续写？在什么时候处理什么请求？redis 单线程处理多个IO请求时就用到了IO多路复用技术。

原理：

如下图，redis 需要处理 3 个 IO 请求，同时把 3 个请求的结果返回给客户端，所以总共需要处理 6 个 IO 事件，由于 redis 是单线程模型，同一时间只能处理一个 IO 事件，于是 redis 需要在合适的时间暂停对某个 IO 事件的处理，转而去处理另一个 IO 事件，这样 redis 就好比一个开关，当开关拨到哪个 IO 事件这个电路上，就处理哪个 IO 事件，其他 IO 事件就暂停处理了。这就是IO多路复用技术。

以上是大致的理解下 IO 多路复用技术，在系统底层，IO 多路复用有 3 种实现机制：select、poll、epoll。

![img](https://pic1.zhimg.com/80/v2-12575653e93cf1b9713535ea9e6d608c_720w.webp)

### **8.3 什么是文件处理器？**

- Redis 基于 Reactor 模式开发了自己的网络事件处理器： 这个处理器被称为文件事件处理器（file event handler）
- 文件事件处理器使用 I/O 多路复用（multiplexing）程序来同时监听多个套接字， 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接应答（accept）、读取（read）、写入（write）、关闭（close）等操作时，与操作相对应的文件事件就会产生，这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
- 文件事件处理器以单线程方式运行，但通过使用 I/O 多路复用程序来监听多个套接字，文件事件处理器既实现了高性能的网络通信模型，又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接， 这保持了 Redis 内部单线程设计的简单性。

**总结与思考**

随着互联网的飞速发展，互联网业务系统所要处理的线上流量越来越大，Redis 的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量，要提升 Redis 的性能有两个方向：

- 优化网络 I/O 模块
- 提高机器内存读写的速度

后者依赖于硬件的发展，暂时无解。所以只能从前者下手，网络 I/O 的优化又可以分为两个方向：

- 零拷贝技术或者 DPDK 技术
- 利用多核优势

**模型缺陷**

Redis 的多线程网络模型实际上并不是一个标准的 Multi-Reactors/Master-Workers模型。
Redis 的多线程方案中，I/O 线程任务仅仅是通过 socket 读取客户端请求命令并解析，却没有真正去执行命令。
所有客户端命令最后还需要回到主线程去执行，因此对多核的利用率并不算高，而且每次主线程都必须在分配完任务之后忙轮询等待所有 I/O 线程完成任务之后才能继续执行其他逻辑。
在我看来，Redis 目前的多线程方案更像是一个折中的选择：既保持了原系统的兼容性，又能利用多核提升 I/O 性能。

原文地址：https://zhuanlan.zhihu.com/p/519383521

作者：linux