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 重写。
notion image
为什么 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 操作仍然很快。
  • 天然的原子性:单线程天然支持所有操作的原子性(如 INCRLPUSH 等),无需额外同步机制,简化了事务(如 MULTI/EXEC)和 Lua 脚本的实现。
Redis 的单线程设计通过 内存速度 + I/O 多路复用 + 无锁原子性 实现了高性能与简单性的平衡。
键值对删除和清空数据库线程的执行过程
主线程收到键值对的异步删除,和异步清空数据库的操作时,主线程会把这个操作封装成一个任务,放入到任务队列中,然后给客戶端返回一个完成信息,表明删除已经完成。但实际上还没删除,后台子线程从任务队列中读取任务后,才开始实际删除键值对, 并释放相应的内存空间。这种删除叫惰性删除(lazy-free)。异步的键值对删除和数据库清空操作需要使用特殊的命令:
  • 键值对删除:使用 UNLINK 命令,不能直接使用 DEL
  • 清空数据库:在 FLUSHDBFLUSHALL 命令后加上 ASYNC 选项
notion image

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

3.1.2 多线程事件驱动

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

因为服务器中同时存在文件事件和时间事件两种事件类型,所以服务器必须对这两种事件进行调度,决定何时应该处理文件事件,何时又应该处理时间事件,以及花多少时间来处理它们等等。
  1. 在程序启动的时候会同时注册文件事件 (监听客户端 socket)和时间事件 (如 serverCron 定时任务)。
  1. 事件的调度和执行由 ae.c/aeProcessEvents 函数负责。将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,这就构成了 Redis 服务器的主函数。在 ae.c/aeProcessEvents 函数里面,对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占。
注意:
  • 优先处理文件事件:,确保快速响应客户端请求。
  • 因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一些。注意避免长时间执行时间事件阻塞事件循环。
Redis系列:高可用架构(哨兵、集群)Redis系列:持久化机制
Loading...