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多路复用技术主要有:
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服务停止。每次循环,调用io多路复用阻塞获取一个就绪的事件列表。遍历列表,依次调用每个事件的注册回调函数,也就是执行上面与其关联的处理器。处理完所有事件列表后,再开始下一次循环。
2.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 线程完成任务之后才能继续执行其他逻辑。
2.3 子线程
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选项
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函数里面,对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占。
因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一些。
- 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!