当前位置: 首页 > 科技观察

面试官:你确定Redis是单线程进程吗?

时间:2023-03-20 21:20:46 科技观察

这次主要分享Redis线程模型篇的面试题。Redis是单线程的吗?什么是Redis单线程模式?为什么Redis单线程那么快?为什么Redis在6.0之前要使用单线程?为什么Redis6.0之后引入多线程?Redis是单线程的吗?Redis单线程是指“接收客户端请求->解析请求->执行数据读写操作->生成数据给客户端”这个过程是由一个线程(主线程)完成的,也就是我们常说的Redis是单线程是有原因的。但是,Redis程序不是单线程的。Redis启动时,会启动一个后台线程(BIO):Redis在2.6版本会启动2个后台线程,分别处理关闭文件和AOF刷写这两个任务。;在Redis4.0版本之后,新增了一个后台线程来异步释放Redis内存,这就是lazyfree线程。比如执行unlinkkey/flushdbasync/flushallasync等命令,会将这些删除操作交给后台线程执行。好处是不会导致Redis主线程卡死。因此,当我们要删除一个大的key时,不要使用del命令来删除,因为del是在主线程上处理的,会导致Redis主线程卡死,所以我们应该使用unlink命令来删除大键异步。Redis之所以创建单独的线程来处理“关闭文件、AOF刷写、释放内存”等任务,是因为这些任务的操作比较耗时。如果这些任务都在主线程上处理,那么Redis主线程很容易被阻塞,导致后续的请求无法处理。后台线程相当于一个消费者。生产者将耗时的任务扔进任务队列,消费者(BIO)不断轮询队列,取出任务后执行相应的方法。关闭文件、AOF刷写、释放内存这三个任务都有自己的任务队列:BIO_CLOSE_FILE,关闭文件任务队列:当队列中有任务时,后台线程会调用close(fd)关闭文件;BIO_AOF_FSYNC、AOFflushingDisk任务队列:当AOF日志配置为everysec选项时,主线程会将AOF日志写操作封装成一个任务放入队列中。当发现队列中有任务时,后台线程会调用fsync(fd)刷入AOF文件,BIO_LAZY_FREE,懒惰释放任务队列:当队列中有任务时,后台线程会free(obj)释放对象/free(dict)delete数据库中的所有对象/free(skiplist)释放skiplist对象;Redis的单线程模式是什么?Redis6.0版本之前的单行模式如下:图中蓝色部分是事件循环,负责主线程。可以看到网络I/O和命令处理都是单线程的。Redis在初始化的时候,这几年会做以下几件事:首先,调用epoll_create()创建一个epoll对象,调用socket()到一个serversocket;然后调用bind()绑定端口,调用listen()监听socket;然后,调用epoll_crt()将listensocket添加到epoll中,并注册“连接事件”处理程序。初始化完成后,主线程进入一个事件循环函数,该函数主要做了以下几件事:首先调用该函数处理发送队列,查看发送队列中是否有任务。如果有发送任务,则通过write函数将客户端发送缓冲区中的数据发送出去。如果这轮数据还没有写完,就会注册写事件处理函数,等待epoll_wait发现可写后再处理。接下来调用epoll_wait函数等待事件的到来:如果connection事件到来,就会调用connectioneventhandler,它会做这些事情:调用accpet获取已连接的socket->调用epoll_ctr添加已连接的socket到epoll->注册“读取事件”处理函数;如果read事件到来,会调用read事件处理函数,该函数会做这些事情:调用read获取客户端发送的数据->解析命令->处理命令->将客户端对象添加到发送队列->将执行结果写入发送缓冲区,等待发送;如果write事件到来,会调用write事件处理函数,write事件处理函数会做这些事情:通过write函数将client发送到sendbuffer如果本轮数据还没有完成,会继续注册write事件处理函数,等待epoll_wait发现可写后再处理。这就是Redis单行模式的工作原理。为什么Redis单线程这么快?官方的benchmark测试结果是单线程Redis的吞吐量可以达到10W/秒,如下图:Redis使用单线程(网络I/O和命令执行)这么快有几个原因:Redis的大部分操作都是在内存中完成的,使用了高效的数据结构。因此Redis的瓶颈可能是机器的内存或者网络带宽,而不是CPU。既然CPU不是瓶颈,自然就采用了单线程的方案。;Redis采用单线程模型,避免了多线程之间的竞争,节省了多线程切换带来的时间和性能开销,也不会产生死锁问题。Redis使用I/O多路复用机制来处理大量的客户端Socket请求。IO多路复用机制就是一个线程处理多个IO流,也就是我们经常听到的select/epoll机制。简单来说,当Redis只运行单线程时,这种机制允许内核中同时存在多个监听Socket和连接Socket。内核会一直监听这些Sockets上的连接请求或数据请求。一旦有请求到达,就会交给Redis线程处理,实现了一个Redis线程处理多个IO流的效果。为什么Redis在6.0之前要使用单线程?我们都知道单线程程序无法利用服务器的多核CPU,那么为什么早期Redis版本的主要工作(网络I/O和命令执行)都使用单线程呢?我们先来看看Redis官方给出的FAQ。核心意思是:CPU不是制约Redis性能的瓶颈,更多的时候是受限于内存大小和网络I/O,所以Redis核心网络模型使用单线程是没有问题的,如果你想使用多核CPU的服务,可以在一台服务器上启动多个节点,也可以使用分片集群。之所以选择单线程,除了上面官方的回答外,还有以下几点考虑。使用单线程后,可维护性高。多线程模型虽然在某些方面表现良好,但引入了程序执行顺序的不确定性,带来了读写并发的一系列问题,增加了系统的复杂度。同时,可能存在线程切换,甚至加锁解锁,死锁等带来的性能损失。为什么Redis6.0之后引入多线程?虽然Redis的主要工作(网络I/O和命令执行)一直都是单线程模型,但是在Redis6.0版本之后,也开始使用多I/O线程来处理网络请求,因为随着网络硬件性能的提升,Redis的性能瓶颈有时会出现在网络I/O的处理上。因此,为了提高网络请求处理的并行性,Redis6.0采用多线程处理网络请求。但是对于读写命令,Redis还是采用单线程来处理,所以不要误会Redis是有多个线程同时执行命令的。据Redis官方介绍,Redis6.0版本中引入的多线程I/O特性至少使性能提升了一倍。Redis6.0版本支持的I/O多线程特性,默认I/O多线程在多线程中只处理写操作(writeclientsocket),不处理读操作(readclientsocket)方式。开启客户端读请求的多线程处理,需要将Redis.conf配置文件中的io-threads-do-reads配置项设置为yes。//读请求也使用io多线程io-threads-do-readsyes同时在redis.conf配置文件中提供了IO多线程个数的配置项。//io-threadsN表示开启N-1个I/O多线程(主线程也算一个I/O线程)。线程数建议设置为2或3。如果是8核CPU,建议线程数设置为6。线程数必须小于机器核心数。线程数越大越好。因此,Redis6.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的压力。