目录
Redis6.0之前是单线程模型
首先我们要明确一个共识,我们通常所说的Redis单线程是指获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这个主线程就是我们平时说的"单线程",而其他的清理脏数据、无用连接的释放、LRU淘汰策略等等也是有其他线程在处理的,因此其实在Redis6之前的Redis本质上也是多线程的。
为什么这些操作要放在同一个主线程中,官方给出的解释:传送门
- 通常瓶颈不在 CPU,而是在内存和网络IO;
- 多线程会带来线程不安全的情况;
- 多线程可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗;
- 单线程降低了Redis内部实现复杂度;
- hash的惰性rehash,lpush等线程不安全的命令可以无锁执行;
文件事件处理器
Redis基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器,file event handler。
Redis 内部使用文件事件处理器
file event handler
,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。
如果被监听的socket准备好执行accept、read、write、close等操作的时候,跟操作对应的文件事件就会产生,这个时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件。
文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了redis内部的线程模型的简单性。
文件事件处理器的结构包含 4 个部分:
- 多个 socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。
文件事件
当socket变得可读时(比如客户端对redis执行write操作,或者close操作),或者有新的可以应答的sccket出现时(客户端对redis执行connect操作),socket就会产生一个AE_READABLE事件。
当socket变得可写的时候(客户端对redis执行read操作),socket会产生一个AE_WRITABLE事件。
IO多路复用程序可以同时监听AE_REABLE和AE_WRITABLE两种事件,要是一个socket同时产生了AE_READABLE和AE_WRITABLE两种事件,那么文件事件分派器优先处理AE_REABLE事件,然后才是AE_WRITABLE事件。
文件事件处理器
如果是客户端要连接redis,那么会为socket关联连接应答处理器
如果是客户端要写数据到redis,那么会为socket关联命令请求处理器
如果是客户端要从redis读数据,那么会为socket关联命令回复处理器
客户端与 Redis 的一次通信过程:
客户端与 Redis 的一次通信过程
要明白,通信是通过 socket 来完成的,不懂的同学可以先去看一看 socket 网络编程。
首先,Redis 服务端进程初始化的时候,会将 server socket 的
AE_READABLE
事件与连接应答处理器关联。客户端 socket01 向 Redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个
AE_READABLE
事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。文件事件分派器从队列中获取 socket,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的AE_READABLE
事件与命令请求处理器关联。假设此时客户端发送了一个
set key value
请求,此时 Redis 中的 socket01 会产生AE_READABLE
事件,IO 多路复用程序将 socket01 压入队列,此时事件分派器从队列中获取到 socket01 产生的AE_READABLE
事件,由于前面 socket01 的AE_READABLE
事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的key value
并在自己内存中完成key value
的设置。操作完成后,它会将 socket01 的AE_WRITABLE
事件与命令回复处理器关联。如果此时客户端准备好接收返回结果了,那么 Redis 中的 socket01 会产生一个
AE_WRITABLE
事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如ok
,之后解除 socket01 的AE_WRITABLE
事件与命令回复处理器的关联。
这样便完成了一次通信。
为啥 Redis 单线程模型也能效率这么高?
Redis 抽象了一套 AE 事件模型,将 IO 事件和时间事件融入一起,同时借助多路复用机制(linux上用epoll) 的回调特性,使得 IO 读写都是非阻塞的,实现高性能的网络处理能力。加上 Redis 基于内存的数据处理,这就是 “单线程,但却高性能” 的核心原因。
- 纯内存操作。
- 核心是基于非阻塞的 IO 多路复用机制。
- C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。
- 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。
Redis单线程问题
IO 数据的读写依然是阻塞的,这也是 Redis 目前的主要性能瓶颈之一,特别是在数据吞吐量特别大的时候,
- 单线程无法利用多CPU
- 串行操作,某个操作“出问题”会“阻塞”后续操作
Redis 6.0 开始引入多线程
注意! Redis 6.0 之后的版本抛弃了单线程模型这一设计,原本使用单线程运行的 Redis 也开始选择性地使用多线程模型。
前面还在强调 Redis 单线程模型的高效性,现在为什么又要引入多线程?这其实说明 Redis 在有些方面,单线程已经不具有优势了。因为读写网络的 Read/Write 系统调用在 Redis 执行期间占用了大部分 CPU 时间,如果把网络读写做成多线程的方式对性能会有很大提升。
Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。 之所以这么设计是不想 Redis 因为多线程而变得复杂,需要去控制 key、lua、事务、LPUSH/LPOP 等等的并发问题。
开启IO多线程
默认情况下,Redis多线程是禁用的,我们可以在redis.conf配置文件选择开启:
#开启IO多线程 io-threads-do-reads yes #配置线程数量,如果设为1就是主线程模式。 io-threads 4
官方建议:至少4核的机器才开启IO多线程,并且除非真的遇到了性能瓶颈,否则不建议开启此配置 ,且配置的线程数少于机器总线程数,如果有4核建议开启2,3个线程,如果有8核建议开6线程。 线程并不是越多越好,多于8个线程意义不大。
总结
Redis 选择使用单线程模型处理客户端的请求主要还是因为 CPU 不是 Redis 服务器的瓶颈,所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本,系统的性能瓶颈也主要在网络 I/O 操作上;而 Redis 引入多线程操作也是出于性能上的考虑,对于一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对 Redis 主线程阻塞的时间,提高执行的效率。
- Redis的多路复用技术,支持epoll、kqueue、selector
- 5.0版本及以前,处理客户端请求的线程只有一个,串行处理
- 6.0版本引入了worker Thread,只处理网络IO读取和写入,核心IO负责串行处理客户端指令
参考链接:
advanced-java/redis-single-thread-model.md at main · doocs/advanced-java · GitHub