Redis | 第6章 事件与客户端《Redis设计与实现》

前言

参考资料:《Redis设计与实现 第二版》;

第二部分为单机数据库的实现,主要由以下模块组成:数据库持久化事件客户端服务器

本篇将介绍 Redis 中的事件与客户端,其中事件有两种:文件事件时间事件;客户端主要介绍属性,以及创建关闭

与本章相关的 Redis 命令总结在下篇文章,欢迎点击收藏,本篇将不再重复:

《Redis常用命令及示例总结(API)》:https://www.jianshu.com/p/f8eb9afaa908


1. 事件

  • Redis 服务器是一个事件驱动程序,服务器需要处理以下两类事件:
    • 文件事件(file event):Redis 服务器通过嵌套字与客户端进行连接,文件事件就是服务器对嵌套字操作的抽象。服务器与客户端的通信会产生响应文件事件,服务器通过监听并处理这些事件来完成一系列网络通信操作;
    • 时间事件(time event):Redis 服务器中的一些操作需要在给定事件点执行,而时间事件就是服务器对这类定时操作的抽象;

1.1 文件事件

1.1.1 I/O 多路复用程序的实现

  • Redis 使用文件事件处理器(file event handler)处理文件事件:

  • 文件事件处理器是基于 Reactor 模式实现的网络通信程序;

  • 文件事件处理器的组成:


    文件事件处理器的组成.png
    • 由四个部分:套接字I/O 多路复用程序文件事件分派器事件处理器
    • I/O 多路复用技术会将所有产生事件的套接字放在一个队列,逐个传送套接字;
  • I/O 多路复用技术的底层实现:

    • 通过包装常见的 select、epoll、evport 和 kqueue 这些 I/O 多路复用函数库来实现;
    • I/O 多路复用函数库实现了相同的API,底层实现跨越互换;
    • 程序会在编译时自动选择系统中性能最高的 I/O 多路复用函数库作为底层实现;
底层 IO 多路复用库的选择.png

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
      连接应答处理器.png
  • 命令请求处理器networking.c/readQueryFromClient
命令请求处理器.png
  • 命令回复处理器networking.c/sendReplyToClient
命令回复处理器.png
  • 客户端与服务端的通信过程
客户端与服务器的通信过程.png

1.2 时间事件

  • 时间事件分两类:
    • 定时事件:事件处理器返回 ae.h/AE_NOMORE
    • 周期性事件:事件处理器返回非 ae.h/AE_NOMORE 的整数值
  • 一个时间事件的三个属性组成:
    • id:唯一标识,新事件 id 大于旧事件 id;
    • when:毫秒精度 UNIX 时间戳,记录时间事件到达(arrive)时间;
    • timeProc:时间事件处理器;
  • 时间事件的实现:服务器将所有时间事件保存在一个无序链表,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用事件处理器;
    • 无序链表不影响事件处理器的性能;
时间事件的实现.png

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 调用后马上返回,不阻塞;
    • 处理所有已产生的文件事件;
    • 处理所有已到达的时间事件;
  • 事件处理角度下的服务器运行流程:


    事件处理角度下的服务器运行流程.png
  • 事件的调度和执行规则:

    • 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;
    };
    
cilents链表.png

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;
    
一个输入缓冲区示例.png
  • 输入缓冲区的大小会根据输入内容动态地缩小或扩大,最大不能超过 1GB;

2.1.5 命令与命令参数

  • 服务器将客户端的命令请求保存到客户端状态的 querybuf 属性后,服务器对命令请求内容分析,得出命令参数及参数个数,保存到客户端状态的 argvargc 属性;

  • argv 属性是一个数组,数组中的每个项都是一个字符串对象,argv[0] 是要执行的命令,之后的其他项是传给命令的参数;

  • argc 属性负责记录 argv 数组的长度;

    typedef struct redisClient{
        //...
        //命令及其参数数组
        robj **argv;
        //数组的长度
        int argc;
    } redisClient;
    
argv 属性与 argc 属性示例.png

2.1.6 命令的实现函数

  • 服务器根据项 argv[0] 的值,在命令表中查找命令所对应的命令实现函数;
命令表.png
  • 在命令表找到 argv[0] 对应的 redisCommand 结构后,会将客户端状态的 cmd 指针指向这个结构;

    typedef struct redisClient{
        //...
        //命令的实现函数
        struct redisCommand *cmd;
    } redisClient;
    
查找命令并设置 cmd 属性.png

2.1.7 输出缓冲区

  • 执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面,每个客户端都有两个输出缓冲区可用:
    • 固定大小的缓冲区:保存长度较小的回复,默认 16KB;

      typedef struct redisClient{
          //...
          //字节数组,保存缓冲区
          char buf[REDIS_REPLY_CHUNK_BYTES];
          //buf已使用的字节数量
          int bufpos;
      }
      
  • 可变大小的缓冲区:保存长度较大的回复,使用链表;

    typedef struct redisClient{
        //...
        //链表
        list *reply;
    } redisClient;
    
可变大小的缓冲区的示例.png

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 链表末尾;
clients 链表.png

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 命令的伪客户端,并在载入完成后,关闭客户端;



最后

\color{blue}{\rm\small{新人制作,如有错误,欢迎指出,感激不尽!}}

\color{blue}{\rm\small{欢迎关注我,并与我交流!}}

\color{blue}{\rm\small{如需转载,请标注出处!}}

最后编辑于:2025-01-11 21:08:17


喜欢的朋友记得点赞、收藏、关注哦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/955132.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot源码解析(七):应用上下文结构体系

SpringBoot源码系列文章 SpringBoot源码解析(一):SpringApplication构造方法 SpringBoot源码解析(二):引导上下文DefaultBootstrapContext SpringBoot源码解析(三):启动开始阶段 SpringBoot源码解析(四):解析应用参数args Sp…

【Flink系列】4. Flink运行时架构

4. Flink运行时架构 4.1 系统架构 Flink运行时架构——Standalone会话模式为例 1)作业管理器(JobManager) JobManager是一个Flink集群中任务管理和调度的核心,是控制应用执行的主进程。也就是说,每个应用都应该被…

thinkphp6 + redis实现大数据导出excel超时或内存溢出问题解决方案

redis下载安装(window版本) 参考地址:https://blog.csdn.net/Ci1693840306/article/details/144214215 php安装redis扩展 参考链接:https://blog.csdn.net/jianchenn/article/details/106144313 解决思路:&#xff0…

HTML中最基本的东西

本文内容的标签,将是看懂HTML的最基本之基本 ,是跟您在写文章时候一样内容。一般想掌握极其容易,但是也要懂得如何使用,过目不忘,为手熟尔。才是我们学习的最终目的。其实边看边敲都行,或者是边看边复制粘贴…

【大前端】Vue3 工程化项目使用详解

目录 一、前言 二、前置准备 2.1 环境准备 2.1.1 create-vue功能 2.1.2 nodejs环境 2.1.3 配置nodejs的环境变量 2.1.4 更换安装包的源 三、工程化项目创建与启动过程 3.1 创建工程化项目 3.2 项目初始化 3.3 项目启动 3.4 核心文件说明 四、VUE两种不同的API风格 …

-bash: /java: cannot execute binary file

在linux安装jdk报错 -bash: /java: cannot execute binary file 原因是jdk安装包和linux的不一致 程序员的面试宝典,一个免费的刷题平台

业务幂等性技术架构体系之消息幂等深入剖析

在系统中当使用消息队列时,无论做哪种技术选型,有很多问题是无论如何也不能忽视的,如:消息必达、消息幂等等。本文以典型的RabbitMQ为例,讲解如何保证消息幂等的可实施解决方案,其他MQ选型均可参考。 一、…

RustDesk ID更新脚本

RustDesk ID更新脚本 此PowerShell脚本自动更新RustDesk ID和密码,并将信息安全地存储在Bitwarden中。 特点 使用以下选项更新RustDesk ID: 使用系统主机名生成一个随机的9位数输入自定义值 为RustDesk生成新的随机密码将RustDesk ID和密码安全地存储…

QT开发技术 【基于TinyXml2的对类进行序列化和反序列化】一

一、对TinyXml2 进行封装 使用宏 实现序列化和反序列化 思路: 利用宏增加一个类函数,使用序列化器调用函数进行序列化 封装宏示例 #define XML_SERIALIZER_BEGIN(ClassName) \ public: \virtual void ToXml(XMLElement* parentElem, bool bSerialize …

金仓Kingbase客户端KStudio报OOM:Java heap space socketTimeout

找到Kingbase\ES\V8\KESRealPro\V008R006C006B0021\ClientTools\guitools\KStudio\KStudio.ini 修改JVM参数: 默认值: -Xms512m -Xmx1024m 改为: -Xms1024m -Xmx2048m -XX:MaxPermSize512m SQL查询报错:An I/O error occurred …

Redis--21--大Key问题解决方案

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言Redis--20--大Key问题解析 一、如何发现Redis大Key1. 使用Redis命令行工具**MEMORY USAGE****RANDOMKEY****DEBUG OBJECT****SCAN命令****redis-cli 工具&#…

【进程与线程】进程的状态

在操作系统中,进程是执行中的程序实例。进程在其生命周期中会经历不同的状态,操作系统根据进程的执行情况和资源调度,将进程划分为多个状态。 这些状态帮助操作系统更加高效地管理 CPU 和系统资源。 进程的状态:就绪态&#xff0…

K8S开启/关闭审计日志

K8S默认禁用审计 开启/关闭 k8s 审计日志 默认 Kubernetes 集群不会输出审计日志信息。通过以下配置,可以开启 Kubernetes 的审计日志功能。 准备审计日志的 Policy 文件配置 API 服务器,开启审计日志重启并验证 准备审计日志 Policy 文件 apiVersio…

nginx: [emerg] bind() to 0.0.0.0:80 failed 端口被占用

nginx: [emerg] bind() to 0.0.0.0:80 failed (10013: An attempt was made to access a socket in a way forbidden by its access permissions) 查看被占用的端口 被系统占用了 HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/HTTP 然后再进入nginx文件夹目录下…

vue2 web 多标签输入框 elinput是否当前焦点

又来分享一点点工作积累及解决方案 产品中需要用户输入一些文字后按下回车键生成标签来显示在页面上&#xff0c;经过尝试与改造完成如下&#xff1a; <template><div class"tags-view" click"beginInput"><el-tag :key"index" …

vLLM私有化部署大语言模型LLM

目录 一、vLLM介绍 二、安装vLLM 1、安装环境 2、安装步骤 三、运行vLLM 1、运行方式 2、切换模型下载源 3、运行本地已下载模型 四、通过http访问vLLM 一、vLLM介绍 vLLM&#xff08;官方网址&#xff1a;https://www.vllm.ai&#xff09;是一种用于大规模语言模型&#x…

STM32的集成开发环境STM32CubeIDE安装

STM32CubeIDE - STM32的集成开发环境 - 意法半导体STMicroelectronics

硬件学习笔记--31 IEC62053-21相关内容介绍

IEC 62053-21是一项由国际电工委员会&#xff08;International Electrotechnical Commission&#xff0c;简称IEC&#xff09;发布的国际标准&#xff0c;全称为《交流电能测量设备&#xff08;a.c.&#xff09;-特殊要求-第21部分&#xff1a;静止式有功能量计量表&#xff0…

C++类与对象(一)—学习记录

序言&#xff1a;要想开发一款成功的应用程序&#xff0c;其开发者必须充分了解并实现用户的需求。作为一个设计良好的类&#xff0c;既要有直观且易于使用的接口&#xff0c;也必须具备高效的实现过程。 一、类与对象基本概念 面向对象程序设计的主要特点为抽象、封装、继承与…

七大排序算法

文章目录 排序的概念及引用1.插入排序2.希尔排序(缩小增量排序)3.选择排序4.堆排序5.冒泡排序6.快速排序7.归并排序8.代码排序部分的测试9.代码加效果大致测试时间&#xff08;仅供参考&#xff09; 排序的概念及引用 排序:将数据按照特定的规律排成递增或递减的操作 稳定性:…