Redis 线程模型
主线程和后台线程
由主线程完成:接收请求->解析请求 ->数据操作->发送数据
主线程和后台线程采用任务队列交互,后台线程包括:
- 关闭文件(
BTO_CLISE_FILE
队列) - AOF 持久化(
BIO_AOF_FSYNC
队列) - 异步释放内存(
BIO_LASY_FREE
队列):lazyfree
线程。unlink
flushdb
flushall
会把删除操作异步执行。
初始化过程
- 创建一个 epoll 对象和一个服务端 socket 对象, 然后绑定端口和监听该 socket;
- 将调用 epoll_ctl() 将 listen socket (文件描述符)加入到 epoll,同时注册「连接事件」处理函数
Note
在 TCP/IP 网络编程中,每次客户端与服务器建立连接,都会在服务器端创建一个新的 客户端 socket(称为 已连接 socket 或 connected socket)。这与服务器端的 监听 socket(listen socket) 完全不同。
事件循环
Reids 处理请求过程:初始化完成后,主线程进入事件循环函数:
-
先看发送缓存区是否有数据,有则发送
-
等待事件到来:
-
连接事件:获取 connected socket -> 将已连接的 socket 加入到 epoll -> 注册读事件(指的是 epoll 监听到 socket 内的数据可读)处理函数;
不注册写事件处理函数,通常默认情况下,socket 总是可写(除非发送缓冲区已满)。注册写事件会导致频繁触发,造成资源浪费。只有当发送缓冲区的数据发送一半无法发送了,说明此时 socket 进入了不可写状态,才需要注册写事件处理函数
-
读事件:调用读事件处理函数:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送;
-
写事件:调用写事件处理函数:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
-
Note
Epoll
epoll
是 Linux 内核中高效的 I/O 多路复用机制。它的高性能主要源于 事件驱动(Event-Driven) 和 高效的数据结构(红黑树 + 双向链表)。
每个文件描述符(fd)在被注册时会被存储在一个 红黑树(RB Tree) 中,只有状态发生变化的文件描述符会被添加到双向链表 Ready List。事件触发(epoll_wait
)只需遍历 Ready List,而非所有文件描述符。
epoll
实现非阻塞(Non-Blocking I/O)的核心在于结合 非阻塞文件描述符 和 事件驱动机制。它通过以下三种机制确保非阻塞 I/O:
- 非阻塞文件描述符(在文件描述符上设置
O_NONBLOCK
标志):在非阻塞模式下,当尝试读取或写入文件描述符时,如果没有数据可读或可写,read()
或write()
函数会立即返回-1
,并设置errno
为EAGAIN
(表示资源暂时不可用),而不会阻塞进程。 - 事件驱动,只有在事件触发时才会通知。
- 如果没有事件准备好,
epoll_wait
会挂起,节省 CPU 资源(而不是空轮询)。
触发模式:
水平触发(Level Triggered,LT,Redis 采用)
- 只要文件描述符的状态保持 就绪(如可读),事件会持续触发。
- 确保数据不会丢失,即使数据没有一次性读取完。
边缘触发(Edge Triggered,ET)
- 只有在状态 变化(从不可用到可用) 时才触发事件。
- 一次性通知,不会重复触发。
多路复用 I/O:
-
一个线程或进程同时监控多个文件描述符(fd)。
-
当文件描述符状态(如可读、可写)发生变化时,立即通知并处理。
-
避免每个 I/O 任务都创建独立线程,节省资源。
为什么快
- 内存完成
- 单线程模型避免竞争和死锁,而CPU 并不是制约 Redis 性能表现的瓶颈所在,更多情况下是受到内存大小和网络I/O的限制。
- I/O 多路复用
Redis 6.0+
单线程局限于数据操作部分,而网络 I/O 引入了多线程模型。默认情况下 I/O 多线程只针对发送响应数据(write client socket),并不会以多线程的方式处理读请求(read client socket)。要想开启多线程处理客户端读请求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes。
至此,Redis 启动的线程包括:
- 主线程
- AOF
- 关闭文件
- 异步释放内存
- 多个网络 I/O 线程