目录
单线程和多线程
redis有多个版本,分别使用了不同的线程策略:(大致情况,特殊版本除外)
- redis4.0之前都是单纯的单线程。
- redis4.0使用了多线程,就是单纯的加上了异步删除。
- redis6.0及之后就是多线程,使用主线程进行命令处理,使用子线程进行多路I/O复用处理网络请求和进行命令解析。(注意,像持久化RDB、AOF、异步删除、集群数据同步等操作也是另起fork线程使用exec执行的)
redis为什么快
单线程模型
要考虑这个问题,就需要先理解单线程redis的工作流程:每个网络请求到达,redis都需要执行一次下述操作:
redis快就快在处理这个流程非常快:
- I/O多路复用:使用了多路复用技术,单个线程即可接收多个socket请求。
- 基于内存操作:Redis 的所有数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能非常高。
- 数据结构简单:Redis 的数据结构都很简单,其查询和操作的时间复杂度大多都是O(1)级别。
- 无上下文切换:使用单线程模型就不用考虑上下文切换和多线程竞争的问题。
其他3个都很好理解,也不用太在意,那么为什么使用I/O多路复用就能很快地处理请求呢?
我们先看一下I/O多路复用的示意图:
可以看到,应用进程在使用epoll建立了监听队列之后,只需等待内核将准备好的文件描述符返回再进行处理即可,而Redis处理网络socket请求也是这个思路:
- 在启动Redis时,会创建一个事件循环(Event Loop)。这个事件循环是负责管理客户端连接的核心部分。
- 在事件循环启动后,Redis会将监听套接字添加到事件循环中,以便监控客户端连接请求。这样,Redis可以同时监听多个套接字,包括监听套接字和已建立的客户端套接字。
- 事件循环进入一个无限循环,不断地等待事件发生。这些事件可以包括新的客户端连接请求、已建立连接上的数据可读事件、已建立连接上的数据可写事件等。
- 当事件循环检测到某个事件发生时,它会调用相应的事件处理函数。对于新的客户端连接请求,事件处理函数会接受连接并将新套接字添加到事件循环。对于已建立连接上的数据可读事件,事件处理函数会读取客户端的命令请求。如果数据可写事件发生,表示可以向客户端发送响应。
- 执行客户端命令。(Redis会按照命令的顺序依次执行它们,确保命令的串行执行,而且Redis可以同时处理多个连接,而不会阻塞在某个特定连接上。)
- 当命令执行完成后,Redis会更新数据结构并将响应写入客户端套接字。如果客户端套接字不可写,Redis会将它加入待写队列,并在后续事件中继续尝试写入。
- 当Redis需要关闭客户端连接时,它将关闭客户端套接字并从事件循环中移除。
命令处理类比图:
有多个redis客户端时,也是这个流程,单线程redis使用I/O多路复用技术对多个socket描述符进行监听,每当有请求来时就让主线程进行处理;当有多个请求同时到达时,就排在epoll的监听队列中等待顺序处理。
使用该I/O多路复用模型后,redis主线程就一直处在接收命令——命令解析——命令执行——回写结果的循环中,而不会由于socket堵塞导致redis处在等待I/O的状态中(要等待socket传输完成后才触发事件),相比IO阻塞模型,这大大减少了IO阻塞时间,因此流程自然很快。
多线程模型
redis引入多线程是为了两个目的:
- 某些命令(如del大key)会导致服务卡顿,需要使用多线程异步删除。
- redis性能受CPU(执行命令的效率)、内存和网络IO(负责处理网络请求的效率)的影响。一般情况下,CPU和内存不会成为Redis的性能瓶颈。当CPU处理不过来底层网络硬件传递的网络请求时,就会导致网络I/O性能瓶颈,所以需要加快CPU处理网络请求的速度,因此需要使用多线程进行网络请求处理。
因此,在redis6.0之后,就使用多线程处理网络请求,使用单线程处理命令(单线程执行命令操作,就不用为了保证Lua脚本、事务的原子性,额外开发多线程互斥加锁机制了,这样一来,Redis线程模型实现就简单了):
引入多线程后处理网络请求的流程:
- 客户端与服务器建立连接,服务器获得socket,然后将socket分配给I/O线程,主线程这时就开始阻塞等待IO线程完成命令读取和解析。
- IO线程并行读取、解析客户端的命令。
- IO线程解析完后,就唤醒主线程进行执行命令。
- 主线程执行完后,将结果写入缓冲区,然后让IO线程回写执行结果到socket,自身就继续阻塞等待请求。
这样redis就抛弃了I/O多路复用模型,而是使用多线程模型解决网络IO问题,同时又使用单线程处理命令,保证了线程安全和高性能。