【Redis】 事件

Metadata

title: 【Redis】 事件
date: 2023-07-09 08:40
tags:
  - 行动阶段/完成
  - 主题场景/数据存储
  - 笔记空间/KnowladgeSpace/ProgramSpace/BasicsSpace
  - 细化主题/数据存储/Redis
categories:
  - 数据存储
keywords:
  - 数据存储/Redis
description: 【Redis】 事件

概述

Redis 服务器是一个事件驱动程序,主要有两种:

  • 文件事件:Redis 服务器通过套接字与客户端连接,文件事件就是服务器对套接字操作的抽象。服务器与客户端通信会产生相应文件事件,服务器通过监听这些事件来完成一系列网络通信操作。
  • 时间事件:Redis 服务器有一些需要在给定时间内执行的操作,而时间事件就是对这类定时操作的抽象。

简单来说,文件事件就是套接字操作相关的事件;时间事件就是定时操作相关事件

  • Redis服务器是一个事件驱动程序,服务器处理的事件分为时间事件文件事件两类。
  • 文件事件处理器是基于Reactor模式实现的网络通信程序
  • 文件事件是对套接字操作的抽象:每次套接字变为可应答(acceptable)、可写(writable)或者可读(readable)时,相应的文件事件就会产生。
  • 文件事件分为AE_READABLE事件(读事件)AE_WRITABLE事件(写事件)两类。
  • 时间事件分为定时事件周期性事件:定时事件只在指定的时间到达一次,而周期性事件则每隔一段时间到达一次。
  • 服务器在一般情况下只执行serverCron函数一个时间事件,并且这个事件是周期性事件。
  • 文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程中也不会进行抢占。
  • 时间事件的实际处理时间通常会比设定的到达时间晚一些。

文件事件

Redis 基于 Reactor 模式开发的网络事件处理器,就是文件事件处理器。大致是使用 I/O 多路复用程序同时监听多个套接字,根据套接字目前执行的任务为套接字关联不同的事件处理器;当被监听的套接字准备好应答,读取,写入,关闭等操作时。与之对应的文件事件就会产生,文件事件处理器就开始发挥作用了,调用事先关联好的事件处理器来处理事件。

利用多路复用,虽然以单线程的方式运行,但文件事件处理器实现了高性能的网络通信模型,又能很好的与 Redis 服务器中其他模块对接,保持了设计的简单性

文件事件处理器的组成

由套接字,I/O 多路复用程序,文件事件分派器,事件处理器组成。

I/O 多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。I/O 多路复用程序总是将所有产生事件的套接字放入到一个队列中,以有序,同步,一次一个套接字向文件事件分派器传送的姿态来运行。只有当上一个套接字产生事件被事件处理器执行完了,才会继续传送下一个套接字。

I/O 多路复用的实现

Redis 为所有多路复用的函数库进行包装,每个多路复用函数库在其中都对应一个单独文件:ae_select.c,ae_epoll.c,ae_kqueue.c。为每个多路复用函数都实现了相同的 API,所以多路复用程序的底层实现是可以互换的。Redis 在多路复用程序源码中用宏定义了相应规则,使得程序在编译时自动选择系统中性能最高的 I/O 多路复用函数库。

事件的类型

多路复用程序可监听的套接字事件可分为ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件。

  • 当套接字变得可读时(客户端对套接字执行 write,close,accept 后),套接字产生 AE_READABLE 事件。
  • 当套接字变得可写时(客户端对套接字执行 read 操作后),套接字产生 AE_WRITABLE 事件。

I/O 多路复用程序允许服务器同时监听者两个事件,如果某个套接字同时产生了两种事件,文件事件分派其会优先处理 AE_READABLE,再处理 AE_WRITABLE

文件事件处理器的事件处理器

根据客户端的需要,事件处理器分为连接应答处理器,命令请求处理器,命令回复处理器,复制处理器。这里只介绍前三者。

  1. 连接应答处理器

networking.c/acceptTcpHandler函数是 Redis 的连接应答处理器,用于连接服务器监听套接字的客户端进行应答。

Redis 服务器初始化时,程序就将连接应答处理器和服务器监听套接字的 AE_READABLE 事件关联,当客户端调用sys/socket.h/connect函数时连接服务器监听套接字时,套接字就会产生 AE_READABLE 事件,引发连接应答处理器执行,并执行相应的套接字应答操作。

简单来说就是客户端连接被监听的套接字时,套接字产生并触发读事件连接应答处理器就会执行

  1. 命令请求处理器

networking.c/readQueryFromClient函数是 Redis 命令请求处理器,主要负责从套接字中读入客户端发送的命令请求内容。

当客户端成功连接到服务器后,服务器会将 AE_READABLE 事件和命令请求处理器关联。当客户端向服务器发送命令请求时,套接字产生 AE_READABLE 事件,引发命令请求处理器执行,执行相应套接字的读入操作。

简单来说就是客户端发送命令请求时,套接字产生并触发读事件命令请求处理器就会执行

  1. 命令回复处理器

networking.c/sendReplyToClient函数是 Redis 的命令回复处理器,负责将服务器执行命令后得到的命令回复通过套接字返回给客户端。

当需要回复命令结果时,服务器会将客户端套接字的 AE_WRITEBLE 事件和命令回复处理器关联,当客户端准备好接收回复时就会产生 AE_WRITABLE 事件,引发命令回复处理器执行。执行结束,服务器会解除命令回复处理器与客户端的套接字 AE_WRITABLE 事件之间的关联。

简单来说就是服务器发送命令回复时,套接字产生并触发写事件命令回复处理器就会执行

总结

一次完整的基于文件事件的服务器与客户端交互,相关处理器的处理过程:

  1. 客户端发起连接,产生读事件,触发连接应答处理器执行。创建套接字,客户端状态并将该套接字的读事件与命令请求处理器关联。
  2. 客户端发送命令,产生读事件,触发命令请求处理器。读取执行命令,得到回复并将该套接字的写事件与命令回复处理器关联。
  3. 客户端读取命令回复,产生写事件,触发命令回复处理器。将回复写入套接字,解除读事件与命令回复处理器的关联。

时间事件

时间事件可分为定时事件周期性事件

  • 定时事件: 只在指定事件到达一次。如 xx 时间后执行一次。
  • 周期性事件: 每隔一段时间执行一次。如每隔 xx 秒执行一次。

注:Redis 一般只用周期性事件。

时间事件的组成

一个时间事件主要由以下三个属性组成:

  • id:服务器为时间事件创建的全局唯一 ID (标识号)。 ID 号按从小到大的顺序递增,新事件的 ID 号比旧事件大。
  • when:毫秒精度的 UNIX 时间戳,时间事件的到达 (arrive) 时间。
  • timeProc:时间事件处理器函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件。

一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值:

  • 事件处理器返回ae.h/AE_NOMORE, 为定时事件:该事件在达到一次之后被删除,之后不再到达。
  • 事件处理器返回非AE_NOMORE的整数值,为周期性时间。当一个时间事件到达之后,服务器会根据事件处理器返回的值,对时间事件的 when 属性进行更新,让这个事件在一段时间之后再次到达,并以这种方式一直更新并运行下去。

实现

服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,遍历整个链表,找到已到达的时间事件,调用相应的事件处理器。新的事件总是插入到链表的表头。

因为事件 ID 只能增大,所以新插入的 id 总是最大的。

serverCron 函数

很多情况下,Redis 需要定期进行资源检查,状态同步等操作,就需要定期操作,而定期操作都是由 serverCron 函数负责的,也是时间事件的应用实例。默认每隔 100ms 执行,具体工作包括:

  • 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。口清理数据库中的过期键值对。
  • 关闭和清理连接失效的客户端。
  • 尝试进行 AOF 或 RDB 持久化操作。
  • 如果服务器是主服务器,那么对从服务器进行定期同步。
  • 如果处于集群模式,对集群进行定期同步和连接测试。

下面简单从几个方面出发,介绍 serverCron 的本职工作。

更新服务器时间缓存

Redis 不少功能依赖于系统当前时间,每次获取系统时间都会进行系统调用,为减少系统调用次数,服务器使用了 unixtime 和 mstime 作为当前时间的缓存。

struct redisServer{

    //保存了秒级精度的系统当前UNIX时间戳
    time_t unixtime;
    //保存了毫秒级的系统当前UNIC时间戳
    long long mstime;
    ...
}

由于 serverCron 默认 100 毫秒更新一次 unixtime 和 mstime,导致其精度不高,只使用于精度要求不高的场景:

  • 服务器打印日志,更新服务器的 LRU 时钟,决定执行持久化,计算上限时间等。
  • 设置过期时间,添加慢查询日志需要高高进度,服务器还是会进行系统调用。

更新 LRU 时钟

每个 Redis 对象也会有 lru 属性,记录上一次被命令访问的时间。如果要计算一个键的空转时长,就要通过 lrulock 记录的时间减去对象的 lru 属性记录时间。

struct redisServer{

    //默认每10秒更新一次的时钟缓存,
    //用于计算键的空转时长
    unsigned lrulock:22;
    ...
}
typedef struct redisObject{
    //对象最后一个被命令访问的时间
    unsigned lru:22;
    ...
}

更新服务器每秒执行命令数

抽样计算函数以 100 毫秒一次,估算最近一秒钟的处理请求数。每次都会根据 4 个变量(上次抽样时间、当前时间、上次抽样已执行命令数、当前已执行命令数)来计算调用之间平均每毫秒处理几个命令,乘以 1000 就是 1 秒内处理命令的估计值。这个估计值会被放入 redisServer 的 ops_sec_samples 数组中。当我们需要知道秒内的指令数时,就会计算这个数组的平均数,因此结果是一个估算值。

更新服务器内存峰值记录

struct redisServer{

    //已使用内存峰值
    size_t stat_peak_memory;
    ...
}

stat_peak_memory 记录内存峰值,每次 serverCron 函数执行就会判断是否需要刷新内存峰值,如果当前使用的多就刷新。

管理客户端资源

serverCron 每次执行都会调用 clientsCron 函数对客户端进行检查:如果已经超时则关闭;如果输入缓冲区大小超过一定长度则重新创建默认大小的输入缓冲区。

管理数据库资源

serverCron 每次执行都会调用 databaseCron 函数,会对服务器的一部分数据库检查,删除过期键;对字典收缩。

执行被延迟的 BGREWRITEAOF

struct redisServer{

    //AOF延迟标志位,如果为1,则有AOF操作被延迟
    int aof_rewrite_shceduled;
    ...
}

aof_rewrite_shceduled标志位决定,如果处于 BGSAVE 命令执行期间,BGREWRITEAOF 会被延迟到 BGSAVE 执行后执行。

检查持久化操作的运行状态

struct redisServer{

    //执行BGSAVE命令的子进程,没有为-1
    pid_t rdb_child_pid;
    //执行BGREWRITEAOF命令的子进程,没有为-1
    pid_t aof_child_pid;
    ...
}

rdb_child_pidaof_child_pid只要一个不为 - 1,则检查子进程是否有信号发来。如果有信号到达则进行后续操作,比如新 RDB 文件的替换,重写的 AOF 文件替换等。

如果rdb_child_pidaof_child_pid都为 - 1,则进行检查:

  • 是否有 BGREWRITEAOF 被延迟,有的话就进行 BGREWRITEAOF 操作。
  • 自动保存条件是否满足,满足且未执行其他持久化操作则执行 BGSAVE。
  • AOF 重写条件是否满足,满足且未执行其他持久化操作则开始一次新的 BGREWRITEAOF 操作。

事件的调度与执行

当服务器同时存在时间事件和文件事件,调度时该如何选择,花费多久?

事件的调度由ae.c/aeProcessEvents函数负责。对于每一次事件循环,主要过程是:

  1. 拿到最近的时间事件并计算还有多少毫秒。
  2. 创建时间任务结构;阻塞等待文件时间产生,最大阻塞时间由最近时间事件到达毫秒数决定
  3. 先处理已产生的文件事件再处理到达的时间事件

执行原则 / 设计利弊:

  1. aeApiPoll 函数(redis 封装的多路复用函数)的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这个方法既可以避免服务器对时间事件进行频繁的轮询 (忙等待),也可以确保 aeApiPoll 函数不会阻塞过长时间
  2. 因为文件事件是随机出现的,如果等待并处理完一次文件事件之后,仍未有任何时间事件到达,那么服务器将再次等待并处理文件事件。
  3. 对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占,因此,不管是文件事件的处理器, 还是时间事件的处理器,它们都会尽可地减少程序的阻塞时间,并在有需要时主动让出执行权,从而降低造成事件饥饿的可能性。