【redis】线程IO模型
Redis线程IO模型
总结:在redis5.0及之前,redis线程io模型是单线程。那么Redis单线程如何处理那么多的并发客户端连接的?原因两点:1)非阻塞io 2)多路复用(事件轮询)
以下,我就针对上面的总结来展开说说redis线程io模型。
1、客户端与redis服务器的通信过程
当客户端执行redis.set(“key”,“value”)命令时,
- 客户端通过操作系统创建一个套接字。
- 这个套接字会连接到运行 Redis 服务器的机器地址和端口(通常是 6379)。
- 一旦连接建立成功,你的程序就可以通过这个套接字向 Redis 服务器发送数据
- 客户端将命令通过
write()
进入 内核写缓冲区。 - 内核写缓存区的数据会从网卡 → redis服务端的内核读缓冲区。
- redis服务端通过多路复用(epoll)得知内核读缓存区有命令需要执行。
- 执行之后的响应数据进入Redis服务器的 内核写缓冲区
- 接着内核写缓存区的数据又会从网卡 → redis客户端的内核读缓冲区。
- 客户端通过
read()
获取响应数据。
当客户端执行redis.get(“key”)命令时,
- 客户端将命令通过
write()
进入 内核写缓冲区。 - 内核写缓存区的数据会从网卡 → redis服务端的内核读缓冲区。
- redis服务端通过多路复用(epoll)得知内核读缓存区有命令需要执行。
- 执行之后的响应数据进入Redis服务器的 内核写缓冲区
- 接着内核写缓存区的数据又会从网卡 → redis客户端的内核读缓冲区。
- 客户端通过
read()
获取响应数据。
GET 命令全流程:
客户端详细视角:
服务端详细视角:
redis服务端视角就能体现Redis高性能的核心:
通过非阻塞IO + 多路复用(epoll/kqueue)单线程监听所有连接的缓冲区状态:
- 当某连接的读缓冲区有数据 → 触发Redis读取命令
- 当某连接的写缓冲区有空闲 → 触发Redis发送响应
此时你可能还不了解非阻塞IO和多路复用究竟是什么?没事,继续看下去。
这里要特别注意,**这里的非阻塞IO是指redis服务器,而非客户端。**因此下面将对比介绍阻塞IO和非阻塞IO,以及多路复用(事件循环)。
2、非阻塞IO & 阻塞IO
阻塞IO:典型的例子就是Java 的 Jedis(同步阻塞客户端)。
jedis.get(“key”) :
-
触发 write():客户端将
GET key
命令写入内核写缓冲区 → 通常瞬间完成- 缓冲区未满:写入数据
- 缓冲区满了:线程卡在
write()
调用(阻塞)
-
触发read():客户端尝试从内核读缓冲区读取 Redis 的响应(如
"value"
)-
有数据:立即返回结果 → 线程继续执行
-
无数据:线程卡在
read()
调用(阻塞)
-
非阻塞IO:典型的例子就是Redis 服务器内部
Redis 将 socket 设置为 Non_Blocking
,
- 调用
read()
时无数据,也会立刻返回EAGAIN
错误而非阻塞。 - 调用
write()
时无空间,也会立刻返回EAGAIN
错误而非阻塞。
这边想要再扩展一个非阻塞IO的例子:
支持非阻塞的客户端库(如 Lettuce)
commands.get()
将命令写入内核写缓冲区(非阻塞写)。- 客户端库 注册回调函数 并立即返回。
- 库内部用 Selector 轮询 内核读缓冲区 → 数据到达后调用回调。
其实,redis只有在感知到内核读缓冲区有数据时,才会调用 read()
去读取数据。
那么redis是怎么感知到内核读缓冲区有数据的?当缓冲区满了,又是怎么知道要什么时候能继续写入数据?答案就是通过多路复用(epoll)。
3、多路复用(epoll)
// Redis 事件循环伪代码
void eventLoop() {while(server.running) {// 0. 计算超时时间(动态值,假设为200ms)timeout = calculate_timeout(); // 1. 获取待处理事件(核心:epoll_wait 在此等待,最多只会阻塞等待timeout时间,如果在这个超时时间内有数据了就会恢复运行态,如果在这个超时时间内依然没有数据,那么就会去处理其他事件,比如时间事件)events = epoll_wait(epoll_fd, events, MAX_EVENTS, timeout); // 2. 处理文件事件(网络请求,处理命令)for each event in events:if event.is_readable: // 可读事件 → 执行客户端命令readQueryFromClient()if event.is_writable: // 可写事件 → 发送响应writeReplyToClient()// 3. 处理时间事件(定时任务,如RDB备份、Key过期)processTimeEvents()}
}
流程图如下:
以上代码,基本可以说明redis单线程都在干什么了:
一个永不停止的循环(while(1)
),用 单线程 同时监听 所有客户端连接 + 定时任务 + 内部任务,通过事件分发处理请求。
这里具体介绍epoll机制:
// Redis 仅通过 epoll 做一件事:
int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, timeout);
- 监听对象:所有客户端连接的 socket
- 监听事件:
EPOLLIN
:内核读缓冲区有数据可读(客户端命令到达)EPOLLOUT
:内核写缓冲区有空闲空间(可发送响应)
epoll 感知事件的底层原理是全程无CPU轮询!依赖硬件中断。
// 系统调用流程
int epoll_wait() {// 1. 检查就绪队列(快速路径)if (!list_empty(rdllist)) return events; // 立即返回// 2. 无事件时:让出CPUset_current_state(TASK_INTERRUPTIBLE); // 标记为阻塞态schedule(); // 主动让出CPU → 其他进程运行
}
也就是说,进入epoll_wait方法,该redis单线程是处于阻塞态的,不占cpu的任何消耗。
这里我还想补充一点,就是之前我学的,redis的rdb持久化以及aof重写过程中会fork一个子进程、aof是开启后台线程刷屏、以及redis4.0出现的懒惰删除也是在后台线程进行的。以上三种情况与redis的单线程又是什么关系呢?
可以这样理解:Redis的「单线程」本质上是对命令执行模型的核心描述,但整个系统确实存在多线程/多进程协作。
BIO后台线程(Background I/O Threads):
- AOF fsync :将AOF缓冲数据刷盘,因为
fsync()
可能阻塞30ms+ - lazy free :异步删除大Key,避免主线程阻塞数秒。(BIO线程删除Key时,主线程已将该Key标记为逻辑删除(移除key的指针的指针引用),BIO只是物理释放内存)
子进程(Child Process):
- AOF重写 触发
BGREWRITEAOF
,fork子进程,父子进程共享内存页,Copy-On-Write 写时复制 - RDB持久化 执行
SAVE
/ 定时保存,fork子进程,父子进程共享内存页,Copy-On-Write 写时复制
总结:主线程独占写操作(100%),而子进程是只读数据,而BIO线程只处理非数据操作。
- AOF重写 触发
BGREWRITEAOF
,fork子进程,父子进程共享内存页,Copy-On-Write 写时复制 - RDB持久化 执行
SAVE
/ 定时保存,fork子进程,父子进程共享内存页,Copy-On-Write 写时复制
总结:主线程独占写操作(100%),而子进程是只读数据,而BIO线程只处理非数据操作。