三. Redis 线程模型
3.1 Redis是单线程吗?
Redis的单线程模型并不是指Redis只有单线程。Redis单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由主线程(单个线程)来完成的。
实际上,Redis不止有这一个线程,还存在**后台线程**:
- Redis 2.6版本有2个后台线程:关闭文件任务线程、AOF刷盘任务线程;
- Redis 4.0版本之后有**3个后台线程**:关闭文件任务线程、AOF刷盘任务线程、释放Redis内存线程;
后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者(BIO)不停轮询这个队列,拿出任务就去执行对应的方法即可。
之所以 Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。
3.2 Redis单线程模式是怎样的?
Redis 6.0之前是单线程模式,如下图:
其中,蓝色部分是一个事件循环,是由主线程负责的,可以看到无论是网络I/O还是命令处理都是由主线程负责的(单线程)。Redis在初始化时会进行以下工作:
- 首先,利用
epoll_create()
创建一个epoll对象,调用socket()
创建一个服务端套接字socket; - 然后,调用
bind()
将socket绑定到指定端口,并使socket进入监听状态; - 最后,调用epoll_ctl()将监听事件加入到epoll中,同时注册事件触发后的处理函数。
代码示例:
epoll_fd = epoll_create(max_connections); // 创建一个 epoll 实例,返回一个 epoll 文件描述符。
// 1. 创建服务端socket,返回socket文件描述符
server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 设置 socket 选项,允许重用本地地址、允许多个套接字绑定同一端口
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
handle_error("setsockopt");
}
// 配置地址和端口信息
address.sin_family = AF_INET; // 地址族为 IPv4(AF_INET)。
address.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0 接受任何传入的网络接口的连接请求
address.sin_port = htons(PORT); // 设置端口号,htons为转换为网络字节序
// 2. 将socket绑定端口
bind(server_fd, (struct sockaddr *)&address, sizeof(address))
// 使套接字进入被动监听状态,等待连接请求
if (listen(server_fd, 3) < 0) { // 最多3个未完成连接(包括在半连接队列和全连接队列中的连接)等待处理
handle_error("listen");
}
// 3. 创建epoll监听事件,加入到epoll实例中
ev.events = EPOLLIN; // 读事件
ev.data.fd = server_fd; // 套接字描述符
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) { // epoll_ctl 注册事件
handle_error("epoll_ctl: server_fd");
}
初始化完后,主线程就进入到一个事件循环函数,主要会做以下事情:
- 首先,先调用处理发送队列函数,看是发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
- 接着,调用 epoll_wait 函数等待事件的到来:
- 如果是**连接事件到来,则会调用连接事件处理函数**,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数;
- 如果是**读事件到来,则会调用读事件处理函数**,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送;
- 如果是**写事件到来,则会调用写事件处理函数**,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
- 注意:以上所有工作都是在主线程中完成的,因此是单线程模型。
3.3 Redis 采用单线程为什么还这么快?
单线程的 Redis 吞吐量可以达到 10W/每秒,如下图所示:
Redis单线程很快的原因主要有以下几点:
- 基于内存:Redis的大多数操作都在内存中完成,且采用了高效的数据结构,因此Redis的瓶颈不是CPU(而可能是机器内存或网络带宽),因此可以直接使用单线程;
- 避免多线程竞争:减少了多线程之间的上下文切换和互斥锁消耗;
- I/O多路复用:通过
epoll
方法来实现多路复用,能够在单线程中监听多个socket上的连接事件。
3.3 Redis 6.0版本后为什么又引入了多线程?
随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络I/O的处理上。网络带宽和传输速度都大幅提高,可能**有大量的网络并发请求**,由于单线程在某个时刻只能去处理新的I/O请求或执行命令中的一个,导致CPU等待I/O操作完成,进而降低整体系统的吞吐量。
为了克服这个瓶颈,Redis 6.0引入了**多I/O线程来处理网络请求。但是对于命令的执行,Redis 仍然使用单线程来处理。Redis 6.0 版本支持的 I/O 多线程特性,默认情况下 I/O 多线程只针对发送响应数据(write client socket)**,并不会以多线程的方式处理读请求(read client socket)。
因此, Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会额外创建 6 个线程(这里的线程数不包括主线程):
-
Redis-server
: Redis的主线程,主要负责执行命令; -
bio_close_file、bio_aof_fsync、bio_lazy_free
:三个后台线程,分别异步处理关闭文件任务、AOF刷盘任务、释放内存任务; -
io_thd_1、io_thd_2、io_thd_3
:三个 I/O 线程,io-threads 默认是 4 ,所以会启动 3(4-1)个 I/O 多线程,用来分担 Redis 网络 I/O 的压力。
资料参考
内容大多直接搬运自:图解Redis介绍 | 小林coding (xiaolincoding.com)