发展历史
在 Redis 2.6 版本中,会启动 2 个后台线程,分别用于处理关闭文件、AOF 刷盘这两个任务。
到了 Redis 4.0 版本之后,又新增了一个名为 “lazy free” 的后台线程,用来异步释放 Redis 内存。例如执行 unlink key、flushdb async、flushall async 等命令,会把这些删除操作交给 “lazy free” 线程来执行,避免导致 Redis 主线程卡顿。
在 Redis 6.0 版本之后,默认情况下会额外创建 6 个线程。其中包括 3 个后台线程,分别是bio_close_file、bio_aof_fsync、bio_lazy_free,继续异步处理关闭文件任务、AOF 刷盘任务、释放内存任务;还有 3 个 I/O 线程,用来分担 Redis 网络 I/O 的压力。
4.0为什么引入多线程
正常情况下使用 del 指令可以很快的删除数据,而当被删除的 key 是一个非常大的对象时,例如包含了成千上万个元素的 hash 集合。这时 del 指令就会造成 Redis 主线程卡顿。
这就是redis3.x单线程时代最经典的故障:大key删除的问题。由于redis是单线程的,当执行 del bigKey 的时候等待很久这个线程才会释放,会阻塞。
所以在Redis 4.0就引入了多个线程来实现数据的异步惰性删除等功能,相关的命令:
unlink key # 异步删除指定的键
flushdb async # 异步删除当前选择数据库中的所有键值对
flushall async # 异步删除 Redis 实例中所有数据库的所有键值对
我们平常说的redis单线程是什么?
Redis是单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取(socket 读)、解析、执行、内容返回(socket 写)等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是Redis对外提供键值存储服务的主要流程。
但Redis的其他功能,比如持久化RDR、AOF、异步删除、集群数据同步等等,其实是由额外的线程执行的。
Redis命令工作线程是单线程的,但是,整个Redis来说,是多线程的。
像redis客户端链接服务端,并且在客户端执行各种set、get命令…这些都是单线程。
redis执行命令依旧保持单线程,redis执行AOF之类的东西是异步线程。
单线程模型的好处:
1.没有上下文切换;
2.不用考虑加锁解锁情况,不会出现死锁问题
IO并发模型都有哪些?
I/O 并发模型是指在处理输入输出(I/O)操作时,为了提高系统的并发性能和资源利用率而采用的各种设计模式和方法。常见的 I/O 并发模型有以下几种:
-
阻塞 I/O 模型(Blocking I/O):这是最基本的 I/O 模型。在这种模型中,当一个线程发起 I/O 操作时,它会一直阻塞等待,直到 I/O 操作完成。例如,在读取文件或网络数据时,线程会被挂起,直到数据准备好或操作完成,期间线程不能执行其他任务,这会导致线程资源的浪费,在处理多个 I/O 操作时效率较低。
-
非阻塞 I/O 模型(Non-blocking I/O):与阻塞 I/O 模型相反,当线程发起 I/O 操作后,不会阻塞等待,而是立即返回。线程可以继续执行其他任务,然后通过轮询等方式不断检查 I/O 操作的状态,看数据是否准备好或操作是否完成。这种模型虽然避免了线程的阻塞,但频繁的轮询会消耗大量的 CPU 资源,也不是一种高效的并发处理方式。
-
I/O 多路复用模型(I/O Multiplexing):该模型通过一个线程来管理多个 I/O 通道,使用 select、poll 或 epoll 等系统调用函数来监听多个文件描述符(I/O 通道)的状态变化。当有 I/O 事件发生时,比如数据可读或可写,系统会通知应用程序,应用程序再对相应的 I/O 通道进行处理。这样可以用较少的线程处理大量的 I/O 操作,提高了系统的并发性能和资源利用率,是目前比较常用的一种 I/O 并发模型,常用于网络服务器等场景。
-
异步 I/O 模型(Asynchronous I/O,AIO):在异步 I/O 模型中,应用程序发起 I/O 操作后,不需要主动去查询操作的状态,而是由操作系统在 I/O 操作完成后通过回调函数或信号等方式通知应用程序。应用程序可以在 I/O 操作进行的同时继续执行其他任务,真正实现了 I/O 操作与其他任务的并行执行,进一步提高了系统的并发性能和响应速度,但实现和管理相对复杂。
-
信号驱动 I/O 模型(Signal-driven I/O):当 Socket 发生 I/O 事件(例如可读、可写)时,操作系统通过信号(例如 SIGIO)通知线程。
Redis 使用了 I/O 多路复用 和 非阻塞式 I/O,这两者是 Redis 实现高并发和低延迟的核心机制。它们从 Redis 的最初版本(1.0,2009年)开始就一直是 Redis 设计的基础,贯穿了 Redis 的整个发展历程。
IO多路复用
I/O 多路复用是一种 事件驱动机制,允许一个线程同时监听多个文件描述符(例如 Socket)的 I/O 事件,从而高效地处理多个并发连接。(IO 多路复用机制也就是select/epoll 机制)
它的核心目标是避免为每个连接分配一个线程(这种方式会消耗大量资源),而是通过一个线程管理所有连接。(一个线程监听多个IO,哪一个IO状态准备就绪了,就处理这个IO,如果没有准备好就跳过这个IO,交出CPU去执行其他准备好的IO)
命令执行阶段,每一条命令并不会立马被执行,而是进入一个一个 socket 队列,当 socket 事件就绪则交给事件分发器分发到对应的事件处理器处理,单线程模型的命令处理如下图所示:
I/O 多路复用的实现:
常见的 I/O 多路复用机制包括:
-
select:最早的多路复用机制,支持的文件描述符数量有限(通常是 1024)。
-
poll:改进版,支持更多文件描述符,但性能仍有限。
-
epoll(Linux):更高效,支持大规模连接,是 Linux 系统下的首选。
-
kqueue(BSD/macOS):类似 epoll,用于 macOS 和 BSD 系统。
-
evport(Solaris):Solaris 系统下的多路复用机制。
I/O 多路复用如何工作?
-
监听多个文件描述符:一个线程通过 I/O 多路复用接口(例如 epoll)监听多个 Socket(文件描述符),包括监听新连接的 Socket 和已连接的客户端 Socket。
-
事件通知:当某个 Socket 发生 I/O 事件时(例如可读事件表示客户端发送了数据,可写事件表示可以发送数据),I/O 多路复用会通知线程。
-
事件处理:线程根据事件类型处理对应的 Socket,例如读取客户端请求、解析命令、执行命令、写回响应。
6.0为什么引入了IO多线程
在单线程模型中,Redis 的主线程既要处理客户端的网络读写(接收请求、发送响应),又要执行命令逻辑。当网络请求量很大时,网络 I/O 的处理速度跟不上,尤其是涉及大量数据传输(如大键值对或高并发小请求),主线程会被阻塞在网络操作上,导致整体吞吐量受限。
为了解决这个问题,从 Redis 6.0 开始引入了多线程,专门用于处理网络 I/O。基本思路是将网络读写任务(例如接收客户端请求和发送响应)交给多个 I/O 线程并行处理,从而提高网络吞吐量和降低延迟。多线程并行处理网络 I/O 可以充分利用多核 CPU 的能力,让 Redis 在高并发场景下更高效地应对客户端请求。
总结:网络 I/O 读写是性能的瓶颈,Redis通过多线程并行处理网络IO提高了性能。
主线程与 I/O 多线程共同协作处理命令架构图:
分析一下这张图:
I/O 多线程并发的接受请求:当多个客户端同时发送请求时,I/O 多线程会并行从 Socket 读取数据并解析命令。例如,客户端 A 发送 GET key1,客户端 B 发送 SET key2 value,I/O 线程可以同时处理这两个请求的网络读取和解析工作,减少网络 I/O 的等待时间。
主线程单线程执行命令:解析后的命令(例如 GET key1 和 SET key2 value)会被放入一个任务队列,主线程按顺序从队列中取出命令并执行。由于主线程是单线程的,命令的执行是串行的,这保证了 Redis 的数据一致性和事务、Lua 脚本等功能的正确性。
写回响应:命令执行完成后,主线程将结果(例如 value1 或 OK)交给 I/O 多线程,I/O 线程再并行地将结果写回客户端的 Socket。
具体流程细节:
Redis 客户端请求连接 Redis 服务端时,连接的建立由主线程处理(而不是多线程)。连接建立后,客户端发送 SET、GET 等命令,这些命令的接收(读取网络数据)由多线程(IO 线程)处理,前提是启用了 Redis 6.0+ 的多线程模式,接收到的命令会被放入一个队列中。在 Redis 服务端,主线程从队列中取出命令,真正执行这些命令(解析和操作内存数据)的部分始终由主线程单线程完成,执行完成后,响应数据通过多线程(IO 线程)发送回客户端。
总结:
Redis 使用单个主线程来执行命令,但是使用多个线程来处理 IO 请求,主线程不再负责包括建立连接、读取数据和回写数据这些事情,而只是专注于执行命令。
IO 多路复用:(6.0之前)
多线程非阻塞式 IO图:(6.0之后)(io多路复用+多线程)
4.0和6.0的区别
4.0——6.0和6.0之后比较:
Redis 6.0 之前:Redis 的网络 I/O、命令解析、命令执行、响应返回等所有操作确实都由一个主线程串行完成。这就是 Redis 经典的单线程模型,强调的是“网络 I/O 和键值对读写(执行)”都在主线程中处理。
Redis 6.0 及以上:Redis 引入了 I/O 多线程,网络 I/O 操作(Socket 读写)可以由多个 I/O 线程并行处理,而命令执行(键值对读写)仍然由主线程单线程完成。
4.0 版本的多线程主要是为了实现一些后台任务的异步处理,例如数据的 异步惰性删除。4.0版本的多线程与 6.0版本的 I/O 多线程的目标和实现方式完全不同,主要是为了优化内存管理的性能,而不是6.0版本的处理网络 I/O。
6.0之后如何开启多线程
注:这里说的开启多线程指的是开启Redis 6.0 及以上版本中引入的 I/O 多线程,即上面的内容。主线程仍旧以单线程方式处理,没有变为多线程。
Redis 6.0 的多线程默认是禁用的,如需开启需要修改 redis.conf 配置文件的配置io-threads-do-reads yes。
开启多线程后,还要设置线程数才能生效,同样是修改 redis.conf配置文件。
io-threads 4
建议:线程数的数量最好小于 CPU 核心数,起码预留一个空闲核处理,因为 Redis 是主线程处理指令,如果系统出现频繁上下文切换,效率会降低。
参考
腾讯云:小红书抗住高并发的背后:Redis 7.0 性能必杀技之 I/O 多线程模型
大话面试:Redis为什么这么快?
小林coding:Redis 常见面试题