type
status
date
slug
summary
tags
category
icon
password

1、概述

Redis 采用事件驱动机制来处理大量的网络IO和命令执行。我们常听到 Redis 是单线程的这种描述,其实就是指 Redis 的事件驱动机制是单线程执行的,而不是指 Redis 中只有一个线程在运行。
什么是事件处理?就是把处理程序当成一个一个的事件处理。IO 的处理过程,可以把一个完整 IO 处理过程分解为一个一个小的任务,可以把这个小的任务叫做事件,处理每个小任务也叫作事件处理。比如把 IO 处理过程分为读事件、计算事件、写事件等各种小的任务进行处理。
Redis 把处理程序抽象为了 2 大类的事件进行处理:
  • 文件事件(file event):Redis 把对网络套接字操作的过程抽象为了各种文件事件。客户端与服务端通信产生的处理程序抽象为相应的文件事件,Redis 服务端通过监听并处理这些文件事件来完成各种网络操作。
  • 时间事件(time eveat):Redis 服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是处理这类定时操作的。

2、文件事件

2.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服务停止。每次循环,调用io多路复用阻塞获取一个就绪的事件列表。遍历列表,依次调用每个事件的注册回调函数,也就是执行上面与其关联的处理器。处理完所有事件列表后,再开始下一次循环。
 
notion image

2.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 线程完成任务之后才能继续执行其他逻辑。

2.3 子线程

notion image
redis除了主线程之外其他的线程可以分为两种
  • 主线启动的时候,会使用操作系统提供的pthread_create函数创建3个子线程,分别负责异步执行以下任务:
    • AOF日志写操作
    • 键值对删除和清空数据库
    • 文件关闭
  • 主从复制或者备份的时候主线程会fork出子线程执行。这些fork线程都使用了cow(写时复制)的技术。
    • 使用bgsave命令,fork出子线程进行rdb备份。
    • 使用bgrewriteaof命令,fork出子线程进行aof重写。
    • 设置主从无盘复制,fork出子线程直接将RDB通过网络发送给从服务器,不需要写到磁盘上。

2.3.1 键值对删除和清空数据库线程的执行过程

主线程收到键值对的异步删除,和异步清空数据库的操作时,主线程会把这个操作封装成一个任务,放入到任务队列中,然后给客戶端返回一个完成信息,表明删除已经完成。但实际上还没删除,后台子线程从任务队列中读取任务后,才开始实际删除键值对, 并释放相应的内存空间。这种删除叫惰性删除(lazy-free)。异步的键值对删除和数据库清空操作需要使用特殊的命令:
  • 键值对删除:使用unlink命令,不能直接使用del
  • 清空数据库:在FLUSHDB和FLUSHALL命令后加上ASYNC选项
notion image

3、时间事件

Redis 的时间事件分为以下两类:
  • 定时事件:让一段程序在指定的时间之后执行一次。比如说,让程序X在当前时间的30毫秒之后执行一次。
  • 周期性事件:让一段程序每隔指定时间就执行一次。比如说,让程序Y每隔30毫秒就执行一次。
服务器所有的时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。正常模式下的Redis服务器只使用serverCron一个周期性的时间事件,而在benchmark模式下,服务器也只使用两个时间事件,所以不影响事件执行的性能。

3.1 serverCron函数的执行

服务器在一般情况下只执行serverCron函数一个时间事件。
持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作由redis.c/serverCron函数负责执行,它的主要工作包括:
  • 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。
  • 清理数据库中的过期键值对。
  • 关闭和清理连接失效的客户端。
  • 尝试进行AOF或RDB持久化操作。
  • 如果服务器是主服务器,那么对从服务器进行定期同步。
  • 如果处于集群模式,对集群进行定期同步和连接测试。
Redis服务器以周期性事件的方式来运行serverCron函数,在服务器运行期间,每隔一段时间,serverCron就会执行一次,直到服务器关闭为止。
在Redis2.6版本,服务器默认规定serverCron每秒运行10次,平均每间隔100毫秒运行一次。
从Redis2.8开始,用户可以通过修改hz选项来调整serverCron的每秒执行次数

4、事件的调度和执行

因为服务器中同时存在文件事件和时间事件两种事件类型,所以服务器必须对这两种事件进行调度,决定何时应该处理文件事件,何时又应该处理时间事件,以及花多少时间来处理它们等等。
事件的调度和执行由ae.c/aeProcessEvents函数负责。将aeProcessEvents函数置于一个循环里面,加上初始化和清理函数,这就构成了Redis服务器的主函数。
在ae.c/aeProcessEvents函数里面,对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占。
因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一些。
Redis高可用Redis持久化
mcbilla
mcbilla
一个普通的干饭人🍚
Announcement
type
status
date
slug
summary
tags
category
icon
password
🎉欢迎来到飙戈的博客🎉
-- 感谢您的支持 ---
👏欢迎学习交流👏