Redis为什么选择单线程
Redis到底是单线程还是多线程?
- Redis的核心业务部分(命令处理)——单线程
- 整个Redis——多线程
在Redis版本迭代过程中,在两个重要的时间节点上引入了多线程的支持:
- Redis v4.0:引入多线程异步处理一些耗时较久的任务,例如异步删除命令unlink
- Redis v6.0:在核心网络模型中引入多线程,进一步提高对于多核CPU的利用率
因此,对于Redis的核心网络模型,在Redis 6.0之前确实都是单线程。是利用epoll(Linux系统)这样的IO多路复用技术在事件循环中不断处理客户端情况。
为什么Redis要选择单线程?
- 抛开持久化不谈,Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升。
- 多线程会导致过多的上下文切换,带来不必要的开销。
- 引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣。
Redis网络模型
Redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装,提供了统一的高性能事件API库AE:

ae.c文件中会根据系统环境选择需要的实现,不同的操作系统会选择不同的epoll实现方式:
1 | /* Include the best multiplexing layer supported by this system. |
启动源码分析
1 | //server.c |
监听事件循环
先看开始监听事件的代码aeMain,其实就是不断轮询进行epoll_wait
,看是否有监听的事件发生,如果有就进行处理:
1 | void aeMain(aeEventLoop *eventLoop) { |
初始化服务
initServer初始化服务,如果是epoll方式的话,相当于是调用了epoll_create
+ epoll_ctl
完成ServerSocket的注册:
1 | void initServer(void){ |
在initServer中除了注册ServerSocket,还需要定义一个acceptTcpHandler,处理客户端连接请求。在客户端连接成功后,该处理器内部会再为该客户端连接注册一个FD读事件并将客户端绑定到读处理器readQueryFromClient上:
1 | //客户端读事件处理器 |
注意,acceptTcpHandler是处理ServerSocket读事件(即客户端的连接请求)的,而其内部定义的readQueryFromClient是用来处理客户端读事件(即客户端发来的web请求)的。
1 | //读处理器 |
processCommand代码如下,其实就是根据命令名称,去字典里找对应的函数实现:
1 | int processCommand(client* c){ |
addReply代码如下,将响应结果写入客户端缓冲区:
1 | void addReply(client* c,robj* obj){ |
此时只是得到了响应并放入队列,还没有真正写出。写出部分需要使用aeEventLoop->beforeSleep前置处理器,每次有客户端事件发生时,都会先调用beforeSleep方法,这个方法负责会遍历server.clients_pending_write这个队列,监听FD写事件并绑定到写处理器sendReplyToClient:
1 | void beforeSleep(struct aeEventLoop * eventLoop){ |
Redis网络模型流程
Redis 单线程网络模型的整体流程如下:
serverSocket
注册到epoll
实例并绑定连接应答处理器tcpAcceptHandler
,开启事件监听循环aeEventLoop
。- 在循环中调用
aeApiPoll
等待 FD 就绪,此时一旦有 FD 就绪,说明有serverSocket
读事件,即有客户端连接请求。 - 收到客户端连接请求后,将客户端的 FD 注册到
epoll
实例,并给客户端绑定命令请求处理器readQueryFromClient
。 - 一旦有客户端连接成功,
aeApiPoll
等待到的 FD 就有两种可能:服务端 socket 或客户端 socket。如果是服务端,说明收到了客户端连接请求,执行第 3 步;如果是客户端,说明是命令请求,需要读取请求数据并返回响应(或异常)。 - 读取请求数据并得到响应的操作需要通过
readQueryFromClient
来完成,主要进行四件事:将请求数据写入客户端缓冲区,解析缓冲区中的数据并转为 Redis 命令,执行命令并将结果写入buf
(容量有上限的数组)或reply
(容量无上限的链表),将client
对象加入队列等待被写出。 - 在每次客户端有事件发生即收到请求时,都会先调用
beforeSleep
方法,为队列中的对象监听 FD 写事件并绑定写处理器,此时aeApiPoll
等待到的事件又多了一种写事件的可能。当队列中对象写就绪时,就会由命令回复处理器sendReplyToClient
负责将响应写出给客户端。
单线程网络模型存在瓶颈:第一个瓶颈是命令请求处理器,它需要从socket读数据并解析;第二个瓶颈是命令回复处理器,它需要将client缓冲区中的数据写入socket。这种网络IO操作的开销都是较大的,至于IO多路复用以及命令执行本身都没有太大的开销。
Redis6.0引入的多线程正是为了解决网络IO的瓶颈,仅在命令解析部分和写出回复部分开启多线程,而命令执行本身和IO多路复用模块依旧使用主线程。多线程模式的整体流程如下:
__END__