Redis学习[2] ——线程模型

三. 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)

  • 33
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值