读书笔记-《Redis设计与实现》(二)单机数据库实现(下)
今天我们来继续学习 Redis 的单机数据库实现部分。
01
事件
Redis 服务器是一个事件驱动程序,其中事件分为文件事件和时间事件,前者是服务器对 Socket 操作的抽象,后者是服务器对定时操作的抽象。
Redis 基于 Reactor 模式开发了自己的文件事件处理器,其组成部分和流程如下:
可以看到,每当一个 Socket 准备好执行连接应答、写入、读取、关闭等操作时,I / O 多路复用程序就会将这些产生事件的 Socket 以有序、同步、每次一个的方式向分派器传送(即当上一个 Socket 所产生的事件被处理完毕后,才会向分派器传送下一个 Socket)。
时间事件分为定时事件和周期性事件,时间事件包含 id、when、timeProc 三个属性,分别为唯一标识符、事件到达时间和到达后要调用的处理器函数。
Redis 服务器将所有时间事件放在一个无序链表(未按照 when 排序)中,执行器运行时将遍历整个链表,调用那些已到达的时间事件的处理器函数。
serverCron 函数就是一个时间事件,它周期性运行并主要完成以下工作:
-
更新服务器的各类统计信息。
-
清理数据库中过期的键值对。
-
关闭和清理连接失败的客户端。
-
尝试进行 AOF 或 RDB 持久化操作。
-
如果是主服务器,则定期进行主从同步。
-
如果是集群模式,则定期对集群进行同步和连接测试。
从 Redis 主函数来看,其对文件事件和时间事件的处理都是同步、有序、原子的,服务器不会中断事件,也不会抢占事件。因此事件处理器都会尽可能减少程序的阻塞时间,在必要时让出执行权,降低事件饥饿的可能性。例如文件事件会在写入超出预设常量的数据时主动 break,下次再写;时间事件也会将非常耗时的持久化操作放到子线程或是子进程中。
02
客户端
Redis 服务器维护了一个链表,保存了所有客户端的状态,其中状态结构的关键属性如下。
typedef struct redisClient {
// 客户端描述符,正常为正整数,如果是伪客户端则为-1
// 如载入AOF和执行Lua脚本的伪客户端
int fd;
robj *name;
// 记录客户端的角色及当前所处状态,多个值通过|分割
// 例如在某些多机功能或执行特点命令时的状态
int flags;
// 用于保存客户端发送命令的输入缓冲区,过大时客户端会被关闭
sds querybuf;
// 服务器将客户端的命令保存到上面的缓冲区后,将解析并放置到以下两个参数
// 分别为命令参数及参数的个数
robj **argv;
int argc;
// 服务器拿到上面的 argv[0],找到命令所对应的实现函数,然后将参数传递过去
struct redisCommand *cmd;
// 执行完命令的结果会放在缓冲区,其中一个大小是固定的,一个是可变的
// 固定的缓冲区用于处理较小的回复,后者则用链表处理大回复
char buf[REDIS_REPLY_CHUNK_BYTES];
int bufpos;
list *reply;
// 是否通过身份验证
int authenticated;
// 创建客户端时间、最近一次互动时间、输出区第一次到达软性限制时间
time_t ctime;
time_t lastinteraction;
time_t obuf_soft_limit_reached_time;
} redisClient;
如前面事件部分所述,当客户端使用 connect 函数连接到服务器时,服务器就会调用连接事件处理器,为客户端创建相应的 redisClient,并添加至链表末尾,客户端就完成了创建。
对于客户端的关闭,除了常见的进程退出、进程杀死、不符合协议格式的命令请求等,输入缓冲区过大和输出缓冲区过大也会导致客户端被服务器关闭(为了避免回复客户端占用服务器过多资源)。其中输出缓冲区包括硬性限制和软性限制,前者是超过一个大小就立即被关闭,后者则是一定时间内连续超过多次则被关闭。
03
服务器
了解了事件和客户端机制后,我们来看看一次命令的执行过程。
-
客户端发送命令请求:用户键入命令,客户端将其转为协议格式,并通过 Socket 发送给服务器。
-
服务器读取命令请求:通过命令请求处理器读取命令,保存到输入缓冲区,分析提取并保存到参数字段,调用命令执行器执行命令。
-
命令执行器查找命令的实现:在命令表 commandtable 中查找命令并保存到客户端状态的 cmd 属性中。
-
命令执行器执行预备操作:一系列前期检查工作。
-
命令执行器调用命令的实现函数:完成后将数据保存到输出缓冲区中。
-
命令执行器执行后续工作:一系列后续工作,例如各种统计、持久化、多机功能。
-
服务器将命令回复发送给客户端:将输出缓冲区的回复发送给客户端,之后清空,为下一次回复做好准备。
-
客户端接收并打印命令回复:转为人类可读的格式并打印。
最后我们再来看看服务器的初始化过程。
-
初始化服务器状态结构:由 initServerConfig 函数来创建一个 redisServer,包括设置默认的运行 ID、运行频率、配置文件路径、运行架构、端口号、RDB / AOF持久化条件、LRU 时钟等等。
-
载入配置选项:如果用户有改动上面的配置,则在这个步骤进行修改。例如上一步中端口默认设置为6379,用户改成了6666,则这一步将其修改。
-
初始化服务器数据结构:由 initServer 函数为各数据结构分配内存,例如 clients 链表、db 数组等等,此外,还会设置进程信号处理器、创建共享对象、打开监听端口、为 serverCron 函数创建时间事件等等。
-
还原数据库状态:通过配置选择使用 AOF 或者 RDB 来还原数据库状态。
-
执行事件循环:开始执行事件。
原文链接:读书笔记-《Redis设计与实现》(二)单机数据库实现(下)
原创不易,点个关注不迷路哟,谢谢~
文章推荐:
-
读书笔记-《当下的力量》
-
读书笔记-《写给大家看的设计书》
-
赛博朋克2077玩后感
-
如何设计离线跑批系统
-
读书笔记-《人人都是产品经理》
-
如何养成好习惯
-
读书笔记-《最好的告别》
-
读书笔记-《Spring技术内幕》(四)事务
-
读书笔记-《Redis设计与实现》(一)数据结构与对象(上)
-
读书笔记-《富爸爸穷爸爸》