前言
参考资料:《Redis设计与实现 第二版》;
第二部分为单机数据库的实现,主要由以下模块组成:数据库、持久化、事件、客户端与服务器;
本篇将介绍 Redis 中的事件与客户端,其中事件有两种:文件事件与时间事件;客户端主要介绍属性,以及创建与关闭;
与本章相关的 Redis 命令总结在下篇文章,欢迎点击收藏,本篇将不再重复:
《Redis常用命令及示例总结(API)》:https://blog.csdn.net/dlhjw1412/article/details/119713214
1. 事件
- Redis 服务器是一个事件驱动程序,服务器需要处理以下两类事件:
- 文件事件(file event):Redis 服务器通过嵌套字与客户端进行连接,文件事件就是服务器对嵌套字操作的抽象。服务器与客户端的通信会产生响应文件事件,服务器通过监听并处理这些事件来完成一系列网络通信操作;
- 时间事件(time event):Redis 服务器中的一些操作需要在给定事件点执行,而时间事件就是服务器对这类定时操作的抽象;
1.1 文件事件
1.1.1 I/O 多路复用程序的实现
- Redis 使用文件事件处理器(file event handler)处理文件事件:
- 文件事件处理器是基于 Reactor 模式实现的网络通信程序;
- 文件事件处理器的组成:
- 由四个部分:套接字、I/O 多路复用程序、文件事件分派器、事件处理器;
- I/O 多路复用技术会将所有产生事件的套接字放在一个队列,逐个传送套接字;
- I/O 多路复用技术的底层实现:
- 通过包装常见的 select、epoll、evport 和 kqueue 这些 I/O 多路复用函数库来实现;
- I/O 多路复用函数库实现了相同的API,底层实现跨越互换;
- 程序会在编译时自动选择系统中性能最高的 I/O 多路复用函数库作为底层实现;
1.1.2 事件类型与 API
- 事件的类型:
- AE_READABLE 事件:(优先)当套接字可读时、或有新的可应答套接字出现时;
- AE_WRITABLE 事件:当套接字变成可写时;
- 相关 API:
ae.c/aeCreateFileEvent
:将给定套接字的给定事件加入到 I/O 多路复用程序的监听范围之内,并对事件和事件处理器进行关联;ae.c/aeGetFileEvents
:接受一个套接字描述符,返回该套接字正在被监听的事件类型;ae.c/aeWait
:在给定时间内阻塞并等待套接字的给定类型事件产生;ae.c/aeApiPoll
:在指定时间内,阻塞并等待所有被 aeCreateFileEvent 函数设置为监听状态的套接字产生文件事件;ae.c/aeProcessEvents
:文件事件分派器;ae.c/aeGetApiName
:返回当前 I/O 多路复用程序底层的多路复用函数库名称;
1.1.3 文件事件的处理器
- Redis 为文件事件编写多个处理器,用于实现不同的网络通信需求,有:
-
连接应答处理器:
networking.c/acceptTcpHandler
;
-
命令请求处理器:
networking.c/readQueryFromClient
;
-
- 命令回复处理器:
networking.c/sendReplyToClient
;
- 客户端与服务端的通信过程:
1.2 时间事件
- 时间事件分两类:
- 定时事件:事件处理器返回
ae.h/AE_NOMORE
; - 周期性事件:事件处理器返回非
ae.h/AE_NOMORE
的整数值
- 定时事件:事件处理器返回
- 一个时间事件的三个属性组成:
- id:唯一标识,新事件 id 大于旧事件 id;
- when:毫秒精度 UNIX 时间戳,记录时间事件到达(arrive)时间;
- timeProc:时间事件处理器;
- 时间事件的实现:服务器将所有时间事件保存在一个无序链表,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用事件处理器;
- 无序链表不影响事件处理器的性能;
1.2.1 API
ae.c/aeCreateTimeEvent
:创建时间事件,这个时间事件将在当前时间的 milliseconds 毫秒后到达,事件处理器为 proc;ae.c/aeDeleteFileEvent
:根据 id 从服务器中删除对应的时间事件;ae.c/aeSearchNearestTimer
:返回到达时间距离当前时间最接近的那个时间事件;ae.c/processTimeEvents
:时间事件执行器,遍历所有时间事件,并调用处理器处理已到达的时间事件。实际不存在,处理时间事件实际由ae.c/aeProcessEvents
函数负责;
1.2.2 serverCron 函数
- Redis 服务器需要定期对自身资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这个操作由
redis.c/serverCron
函数执行,其主要工作包括:- 更新服务器的各类信息,如时间、内存占用、数据库占用情况;
- 清理数据库中的过期键值对;
- 关闭和清理连接失败的客户端;
- 尝试进行 AOF 或 RDB 持久化操作;
- 如果服务器是主服务器,需要对服务器进行定期同步;
- 如果服务器是集群模式,对集群进行定期同步和连接测试;
1.3 事件的调度与执行
- Redis 服务器对文件事件与时间事件的调度由
ae.c/aeProcessEvents
函数负责; ae.c/aeProcessEvents
函数的处理逻辑:- 获取到达时间离当前时间最接近的时间事件;
- 计算最接近的时间事件距离到达还有多少毫秒 remaind_ms;
- 事件到达则将 remaind_ms 设为0;
- 根据 remaind_ms 的值创建 timeval 结构;
- 阻塞并等待文件事件产生,最大阻塞事件由传入的 timeval 结构决定;
- 若 remaind_ms 的值为0,那么 aeApiPoll 调用后马上返回,不阻塞;
- 处理所有已产生的文件事件;
- 处理所有已到达的时间事件;
- 事件处理角度下的服务器运行流程:
- 事件的调度和执行规则:
aeApiPoll
函数的最大阻塞时间由到达时间最接近当前时间的事件事件决定;- 文件事件和时间事件的处理都是同步、有序、原子地执行,服务器不会中途中断事件处理,也不会对事件进行抢占;
- 时间事件会将非常耗时的持久化操作放到子线程或子进程执行;
- 时间事件在文件事件之后执行,并且事件间不会出现抢占,所以时间事件的实际处理时间通常比时间事件的到达时间稍晚一些;
1.4 Redis 的单进程单线程误区
- Redis 并不完全是单进程单线程的,在进行 RBD 持久化,执行 BGSAVE 命令时,会创建一个子进程在后台进行备份;
- Redis 的单进程指的是:文件事件处理器是单进程单线程模式运行的,也即 Redis 处理大部分请求时是用单线程;
- Redis 使用单线程模式还能保持高效率,主要是因为:
- 纯内存操作(主要);
- 核心是基于非阻塞的 I/O 多路复用机制(主要);
- 单线程避免了多线程的频繁上下文切换问题;
- I/O 多路复用程序总是将所有产生事件的套接字放到一个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字;
- Redis 的 I/O 多路复用程序通过包装常见的底层 I/O 多用复用函数库实现,程序在编译时自动选择系统中性能最高的 I/O 多路复用函数库作为底层实现;
2. 客户端
-
Redis 服务器状态结构 clients 属性是一个链表,保存了所有客户端状态:
struct redisServer{ //... //客户端链表 list *clients; };
2.1 客户端属性
- 客户端状态包含的属性可以分为两类:
- 通用属性:很少与特定功能有关,本章重点介绍;
- 与特定功能相关的属性:如操作数据库时的 db 属性与 dicrid 属性,执行事务时需要的 mstate 属性,执行 WATCH 命令时需要用到的 watched_keys 属性等;
2.1.1 嵌套字描述符
- 客户端状态的 fd 属性记录了客户端正在使用的嵌套字描述符:
typedef struct redisClient{ //... //嵌套字描述符 int fd; } redisClient;
- fd 为 -1 时:伪客户端,命令请求来源于 AOF 文件或 Lua 脚本,不需要套接字。用于载入 AOF 文件并还原数据库状态和执行 Lua 脚本中包含的 Redis命令;
- fd 大于 -1 时:普通客户端,服务器使用
fd
属性记录客户端套接字的描述符;
2.1.2 名字
- 默认情况,一个连接到服务器的客户端是没有名字的;
- 可以使用 CLIENT SETNAME 命令为客户端设置一个名字;
- 使用 CLIENT GETNAME 命令获取当前连接的名字;
- 客户端名字记录在客户端状态的 name 属性里:
typedef struct redisCilent{ //... //名字 robj *name; } redisClient;
2.1.3 标志
-
客户端的标志属性 flags 记录了客户端的角色,以及客户端目前所处的状态:
typedef struct redisClient{ //... //标志 int flags; } redisClient;
- flags属性可以是单个标志,也可以是多个二进制;
-
每个标志使用一个常量表示,一部分记录了客户端角色。另一部分标志记录了客户端目前所处状态;
-
通常情况下,Redis 只会将那些对数据库进行修改的命令写入 AOF 文件,并复制到各个从服务器;
-
PUBSUB 和 SCRIPT LOAD 命令除外,带有副作用,前者会改变接受消息的客户端状态,服务端需要使用 REDIS_FORCE_AOF 标志。后者修改服务器状态,服务器状态需要REDIS_FORCE_AOF 和 REDIS_FORCE_REPL 标志;
-
标志在
redis.h
文件里面定义;
2.1.4 输入缓冲区
-
客户端状态的输入缓冲区用于保存客户端发送的命令请求;
typedef struct redisClient{ //... //输入缓冲区 sds querybuf; } redisClient;
- 输入缓冲区的大小会根据输入内容动态地缩小或扩大,最大不能超过 1GB;
2.1.5 命令与命令参数
-
服务器将客户端的命令请求保存到客户端状态的
querybuf
属性后,服务器对命令请求内容分析,得出命令参数及参数个数,保存到客户端状态的argv
和argc
属性; -
argv
属性是一个数组,数组中的每个项都是一个字符串对象,argv[0]
是要执行的命令,之后的其他项是传给命令的参数; -
argc
属性负责记录argv
数组的长度;typedef struct redisClient{ //... //命令及其参数数组 robj **argv; //数组的长度 int argc; } redisClient;
2.1.6 命令的实现函数
- 服务器根据项
argv[0]
的值,在命令表中查找命令所对应的命令实现函数;
-
在命令表找到
argv[0]
对应的redisCommand
结构后,会将客户端状态的cmd
指针指向这个结构;typedef struct redisClient{ //... //命令的实现函数 struct redisCommand *cmd; } redisClient;
2.1.7 输出缓冲区
- 执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面,每个客户端都有两个输出缓冲区可用:
-
固定大小的缓冲区:保存长度较小的回复,默认 16KB;
typedef struct redisClient{ //... //字节数组,保存缓冲区 char buf[REDIS_REPLY_CHUNK_BYTES]; //buf已使用的字节数量 int bufpos; }
-
可变大小的缓冲区:保存长度较大的回复,使用链表;
typedef struct redisClient{ //... //链表 list *reply; } redisClient;
-
2.1.8 身份验证
-
客户端状态的
authenticated
属性用于记录客户端是否通过了身份验证;typedef struct redisClient{ //... //身份验证属性 int authenticated; } redisClient;
- authenticated 为 0 时:身份未验证;
- authenticated 为 1 时:身份验证;
-
authenticated
属性仅在服务器启用身份验证功能时用。若该功能未开启,即使authenticated
值为 0,服务器也不会拒绝客户端的命令请求;
2.1.9 时间
typedef struct redisClient{
//...
//记录了创建客户端的时间
time_t ctime;
//记录客户端与服务器最后一次进行互动(interaction)的时间,即空转时间
time_t lastinteraction;
//输出缓冲区第一次到达软性限制(soft limit)的时间
time_t obuf_soft_limit_reached_time;
} redisClient;
2.2 客户端的创建与关闭
2.2.1 创建普通客户端
- 客户使用 connect 函数连接服务器时,服务器调用连接事件处理器,为客户端创建相应的客户端状态,并将新的客户端状态添加到服务器状态结构 clients 链表末尾;
2.2.2 关闭普通客户端
-
一个普通客户端可以因为多种原因而被关闭;
- 如:网络连接关闭、发送了不合协议格式的命令请求、成为 CLIENT KILL 命令的到目标、空转时间超时、命令请求超过输入缓冲区限制大小(1 GB)、输出缓冲区的大小超出限制;
-
服务器使用两种模式限制客户端输出缓冲区的大小:
- 硬性模式(hard limit):缓冲区大小超过所硬性限制所设置大小,立即关闭客户端;
- 软性模式(soft limit):缓冲区大小超过软性限制所设置大小,并且持续时间超过设置的时间,才会关闭客户端;
2.2.3 Lua 脚本的伪客户端
-
服务器在初始化时创建负责执行 Lua 脚本中包含的 Redis 命令的伪客户端,并将其关联在服务器状态结构的
lua_client
属性;struct redisServer{ //... //Lua 脚本的伪客户端 redisClient *lua_client; };
-
伪客户端在服务器运行的整个生命周期中会一直存在,只有服务器被关闭时,这个客户端才会被关闭;
2.2.4 AOF 文件的伪客户端
- 服务器在载入 AOF 文件时,会创建用于执行 AOF 文件中包含的 Redis 命令的伪客户端,并在载入完成后,关闭客户端;
最后
![](https://i-blog.csdnimg.cn/blog_migrate/4c125f9add4205c2c8b94d529690a7c5.jpeg)