type
status
date
slug
summary
tags
category
password
1、概述
我们常听到 Redis 是单线程的描述,其实是指 Redis 的事件驱动机制是单线程执行的,而不是指 Redis 中只有一个线程在运行。更准确来说,Redis 采用基于单线程的事件驱动机制来处理网络 IO 和命令执行。
这里涉及到两个概念:
- Redis 的线程模型是怎么样的?
- 事件驱动机制是什么?
2、Redis的线程模型
Redis 的线程可以分为三种:
- 主线程:单线程(6.0 之后引入多线程优化,但核心处理仍是单线程),实现事件驱动机制。负责处理以下任务:
- 命令执行:所有命令在单线程中顺序执行,保证原子性
- 网络 I/O:6.0 前单线程处理,6.0 后多线程处理网络读写
- 定时任务:由主线程处理时间事件
- I/O线程:6.0 以后引入多线程负责处理网络读写,默认不启用(需配置)
- 后台线程:根据执行的任务类型可以分为两种:
- 执行后台任务:主线启动的时候,会使用操作系统提供的
pthread_create
函数创建 3 个后台子线程,分别负责异步执行以下任务: - AOF 日志写操作
- 键值对删除和清空数据库
- 文件关闭
- 执行主从复制或持久化任务:主从复制或者持久化操作可以选择后台执行,主线程会 fork 出子线程执行任务。
- 主从复制:fork 出子线程直接将 RDB 通过网络发送给从服务器,不需要写到磁盘上。
- RDB 备份:使用
BGSAVE
命令,fork 出子线程进行 RDB 备份。 - AOF 重写:使用
BGREWRITEAOF
命令,fork 出子线程进行 AOF 重写。

为什么 Redis 要采用单线程模型?
先明确一个概念,Redis 单线程是指 Redis 处理命令执行和网络 I/0 这些核心逻辑是单线程的,而不是 Redis 中只有一个线程在执行。
Redis 采用单线程设计的原因:
- 基于内存的快速访问:Redis 的数据全部存储在内存中,内存的读写速度极快(纳秒级),单线程顺序执行命令的吞吐量已经足够高,多线程的收益不明显。
- 避免锁竞争和上下文切换:单线程无需考虑多线程的并发问题,简化了数据结构的实现(如内存分配、哈希表操作等),无需考虑并发控制(如互斥锁、原子操作等),还可以避免多线程频繁切换消耗 CPU 资源。
- 基于 I/O 多路复用(Non-blocking I/O):Redis 使用 epoll/kqueue 等系统调用实现高效的 I/O 多路复用,单线程可以同时监听大量客户端连接(事件驱动),通过事件循环(Event Loop)处理请求。网络 I/O 的耗时(如等待客户端请求)不会阻塞线程,实际执行命令的 CPU 操作仍然很快。
- 天然的原子性:单线程天然支持所有操作的原子性(如
INCR
、LPUSH
等),无需额外同步机制,简化了事务(如MULTI/EXEC
)和 Lua 脚本的实现。
Redis 的单线程设计通过 内存速度 + I/O 多路复用 + 无锁原子性 实现了高性能与简单性的平衡。
键值对删除和清空数据库线程的执行过程
主线程收到键值对的异步删除,和异步清空数据库的操作时,主线程会把这个操作封装成一个任务,放入到任务队列中,然后给客戶端返回一个完成信息,表明删除已经完成。但实际上还没删除,后台子线程从任务队列中读取任务后,才开始实际删除键值对, 并释放相应的内存空间。这种删除叫惰性删除(lazy-free)。异步的键值对删除和数据库清空操作需要使用特殊的命令:
- 键值对删除:使用
UNLINK
命令,不能直接使用DEL
- 清空数据库:在
FLUSHDB
和FLUSHALL
命令后加上ASYNC
选项

3、事件驱动机制
Redis 单线程的核心是高效的事件驱动机制。事件驱动机制就是把处理过程拆解成一个个事件处理。比如把一个完整 IO 处理过程分为读事件、计算事件、写事件等各种小的任务进行处理。
事件循环机制包含以下内容:
- 事件循环 (Event Loop):Redis 使用 I/O 多路复用技术来监听多个文件描述符,主循环不断检查是否有事件发生并处理相应事件。
- 事件处理:Redis 把事件分为两种类型:
- 文件事件(file event):处理网络 I/0。Redis 把对网络套接字操作的过程抽象为了各种文件事件。客户端与服务端通信产生的处理程序抽象为相应的文件事件,Redis 服务端通过监听并处理这些文件事件来完成各种网络操作。
- 时间事件(time eveat):处理定时任务。Redis 服务器中的一些操作(比如 serverCron 函数)需要在给定的时间点执行,而时间事件就是处理这类定时操作的。
3.1 文件事件
3.1.1 单线程事件驱动
Redis 基于 Reactor 模型实现了一套事件驱动库,主要包含下面四部分:
- 套接字:文件事件是对套接字操作的抽象,每当一个套接字准备好执行 accept、read、write和 close 等操作时,就会产生一个文件事件。因为 Redis 通常会连接多个套接字,所以多个文件事件有可能并发的出现。
- IO 多路复用程序:I/O多路复用程序负责监听多个套接字,并向文件事件派发器传递那些产生了事件的套接字。IO多路复用技术主要有:
select
、epoll
、evport
和kqueue
等,Redis 会根据不同的操作系统选择最优的 IO 多路复用技术实现。
- 文件事件分派器:文件事件分派器接收 I/O 多路复用程序传来的套接字,并根据套接字产生的事件的类型,调用相应的事件处理器。服务器会为执行不同任务的套接字关联不同的事件处理器,这些处理器是一个个函数,它们定义了某个事件发生时,服务器应该执行的动作。
- 事件处理器:Redis 为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求。
- 连接应答处理器:用于对连接服务器监听套接字的客户端进行应答。当 Redis 服务器进行初始化的时候,程序会将这个连接应答处理器和服务器监听套接字的
AE_READABLE
事件关联起来,当有客户端用 sys/socket.h/connect 函数连接服务器监听套接字的时候,套接字就会产生AE_READABLE
事件,引发连接应答处理器执行,并执行相应的套接字应答操作 - 命令请求处理器:负责从套接字中读入客户端发送的命令请求内容。当一个客户端通过连接应答处理器成功连接到服务器之后,服务器会将客户端套接字的
AE_READABLE
事件和命令请求处理器关联起来,当客户端向服务器发送命令请求的时候,套接字就会产生AE_READABLE
事件,引发命令请求处理器执行,并执行相应的套接字读入操作。 - 命令回复处理器:负责将服务器执行命令后得到的命令回复通过套接字返回给客户端。当服务器有命令回复需要传送给客户端的时候,服务器会将客户端套接字的
AE_WRITABLE
事件和命令回复处理器关联起来,当客户端准备好接收服务器传回的命令回复时,就会产生AE_WRITABLE
事件,引发命令回复处理器执行,并执行相应的套接字写入操作。当命令回复发送完毕之后,服务器就会解除命令回复处理器与客户端套接字的AE_WRITABLE
事件之间的关联。
实际上我们所说的 Redis 单线程只是针对 Redis 网络请求模块,即上面提到的文件事件处理器。

Redis 服务器处理客户端命令请求的完整过程如下:
- 服务端启动:在server.c#main(),redis服务器在初始化时打开监听端口,等待客户端的命令请求,并且为TCP 连接关联连接应答(accept)处理器。
- 客户端连接:客户端向 redis 的 server socket 请求建立连接,此时 server socket 会产生一个
AE_READABLE
事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件和应答处理器关联起来,然后将该事件压入队列中。
- 客户端发起读请求:客户端向 redis 的 server socket 发起读请求,此时 server socket 会产生一个
AE_READABLE
事件,IO 多路复用程序将该事件与命令请求处理器关联,然后将该事件压入队列中。
- 服务端回复请求:如果此时客户端准备好接收返回结果了,此时 server socket 会产生一个
AE_WRITABLE
事件,IO 多路复用程序将该事件与命令回复处理器关联,然后将该事件压入队列中。
- 文件分派器处理请求:在
ae.c#aeMain
里面,在主线程里面调用一个 while 循环,这个 while 循环会一直循环直到 Redis 服务停止。每次循环,调用 I/0 多路复用阻塞获取一个就绪的事件列表。遍历列表,依次调用每个事件的注册回调函数,也就是执行上面与其关联的处理器。处理完所有事件列表后,再开始下一次循环。

3.1.2 多线程事件驱动
Redis 6.0 之后支持多线程,其实只是把网络 IO 使用多个子线程去完成,处理命令等其他工作仍然在主线程上执行。步骤如下:
- 服务端和客户端建立 Socket 连接,并分配处理线程。首先,主线程负责接收建立连接请求。当有客户端请求和实例建立 Socket 连接时,主线程会创建和客户端的连接,并把 Socket 放入全局等待队列中。紧接着,主线程通过轮询方法把 Socket 连接分配给 IO 线程。
- IO 线程读取并解析请求。 主线程一旦把 Socket 分配给 IO 线程,就会进入阻塞状态,等待 IO 线程完成客户端请求读取和解析。因为有多个 IO 线程在并行处理,所以,这个过程很快就可以完成。
- 主线程执行请求操作。等到 IO 线程解析完请求,主线程还是会以单线程的方式执行这些命令操作。
- IO 线程回写 Socket 和主线程清空全局队列。当主线程执行完请求操作后,会把需要返回的结果写入缓冲区,然后,主线程会阻塞等待 IO 线程把这些结果回写到 Socket 中,并返回给客户端。和 IO 线程读取和解析请求一样,IO 线程回写 Socket 时,也是有多个线程在并发执行,所以回写 Socket 的速度也很快。等到 IO 线程回写 Socket 完毕,主线程会清空全局队列,等待客户端的后续请求。

单线程和多线程比较
- 单线程:IO复用、文件分发(只有一个线程实际不需要分发)、网络数据读取(read系统调用)、解析并执行查询命令、网络数据输出(write系统调用)这些功能都是由主线程完成的。
- 多线程:I/O 线程任务仅仅是通过 socket 读取客户端请求命令并解析,却没有真正去执行命令。所有客户端命令最后还需要回到主线程去执行,因此对多核的利用率并不算高,而且每次主线程都必须在分配完任务之后忙轮询等待所有 I/O 线程完成任务之后才能继续执行其他逻辑。
3.2 时间事件
Redis 的时间事件分为以下两类:
- 一次性事件:让一段程序在指定的时间之后执行一次。
- 周期性事件:让一段程序每隔指定时间就执行一次。
服务器所有的时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
正常情况下的 Redis 服务器只执行 serverCron 一个周期性的时间事件(即使在 benchmark 模式下,服务器也只使用两个时间事件,所以不影响事件执行的性能)。serverCron 函数的作用是对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行。它的具体工作包括:
- 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。
- 清理数据库中的过期键值对。
- 关闭和清理连接失效的客户端。
- 尝试进行 AOF 或 RDB 持久化操作。
- 如果服务器是主服务器,那么对从服务器进行定期同步。
- 如果处于集群模式,对集群进行定期同步和连接测试。
Redis服务器以周期性事件的方式来运行 serverCron 函数,直到服务器关闭为止。
- Redis 2.6 版本,serverCron 默认每秒运行 10 次,即每间隔 100 毫秒运行一次。
- Redis 2.8 版本开始,用户可以通过修改
hz
选项来调整 serverCron 的每秒执行次数
3.3 事件的调度和执行
因为服务器中同时存在文件事件和时间事件两种事件类型,所以服务器必须对这两种事件进行调度,决定何时应该处理文件事件,何时又应该处理时间事件,以及花多少时间来处理它们等等。
- 在程序启动的时候会同时注册文件事件 (监听客户端 socket)和时间事件 (如 serverCron 定时任务)。
- 事件的调度和执行由
ae.c/aeProcessEvents
函数负责。将aeProcessEvents
函数置于一个循环里面,加上初始化和清理函数,这就构成了 Redis 服务器的主函数。在ae.c/aeProcessEvents
函数里面,对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占。
注意:
- 优先处理文件事件:,确保快速响应客户端请求。
- 因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一些。注意避免长时间执行时间事件阻塞事件循环。
- Author:mcbilla
- URL:http://mcbilla.com/article/46763f8d-8230-4bb5-bcef-fb054fdc89b7
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!