Redis设计与实现-底层实现

news/2025/3/28 4:34:33/

Redis底层实现

  • 1、事件
    • 1.1 文件事件
    • 1.2 时间事件
    • 1.3 事件调度
  • 2、Redis客户端
    • 2.1 客户端的相关属性
    • 2.2 客户端的创建与关闭
      • 2.2.1 普通客户端的创建
      • 2.2.2 普通客户端的关闭
      • 2.2.3 AOF的伪客户端
      • 2.2.4 Lua脚本的伪客户端
  • 3、Redis服务端
    • 3.1 命令请求的执行过程
      • 3.1.1 客户端发送命令请求
      • 3.1.2 读取命令请求
      • 3.1.3 执行实现函数
      • 3.1.4 将命令回复发送给客户端
    • 3.2 serverCron函数
    • 3.3 初始化服务器
      • 3.3.1 初始化服务器的状态结构redisServer
      • 3.3.2 载入配置选项
      • 3.3.3 初始化服务器中的数据结构
      • 3.3.4 还原数据库状态
      • 3.3.5 执行事件循环

如有侵权,请联系~
如有错误,也欢迎批评指正~
本篇文章大部分是来自学习《Redis设计与实现》的笔记

1、事件

在Redis设计与实现-数据持久化中提到了处理文件事件和时间时间,这里进行详细说明。

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

  • 文件事件:Redis服务器通过socket与客户端进行连接,而文件事件就是服务器对socket操作的抽象。服务器与客户端通信产生相应的文件事件,服务器通过监听并处理这些事件完成一系列网络通信。我理解文件通信主要是负责客户端的请求。
  • 时间时间:Redis存在一些操作,如定时操作serverCron函数,需要在指定时间点执行某些操作。而时间时间就是这类定时操作的抽象。

1.1 文件事件

Redis是基于reactor模式开发的自己的网络事件处理器。
IO多路复用模块同时监听多个客户端,并且将请求socket存储到请求队列中,等待文件事件分发器进行分发。
文件事件分派器根据事件的类型,调用相应的连接处理器进行处理。这些处理器其实就是一个个函数,执行不同的动作、逻辑。
在这里插入图片描述

1.2 时间事件

Redis的时间事件分为两种:

  • 定时事件:程序在指定的时间后执行。
  • 周期性事件:程序每隔一段时间执行一次。

一个时间事件有三个属性:

  • id:一个时间事件的全局唯一标识,这个是递增的。
  • when:事件的到达时间,毫秒精度的unix时间戳。
  • timeProc:时间事件处理器。当时间到达的时候调用相应的处理器进行处理事件【一个事件是定时还是周期性的根据时间事件处理器的返回结果判断。如果返回AE_NOMORE则就是定时的,如果是返回其他的整数就是周期性的。例如返回30,则表示30ms之后再次执行】。

redis中时间事件是以无序【是指的不按照执行时间的大小排序】列表的形式存在的。
在这里插入图片描述
时间事件执行器遍历所有的时间事件,如果当天事件大于等于某个事件的when,则就会调用相应的处理器。如果处理器返回的是AE_NOMORE(说明这个事件是定时的),则就会将这个时间事件从列表中删除;否则就会更新这个事件的when属性。

目前正常的redis只有一个serverCron定期时间事件【Redis2.6执行频率100ms,Redis2.8版本可以通过hz参数进行设置】。serverCron为了保证redis服务器能够正常、稳定的运行,需要对服务器状态进行检查和调整,工作主要包括:

  • 定期删除过期的键值对
  • 进行RDB或者AOF持久化
  • 更新服务器的统计信息,如内存占用、数据库占用等
  • 关闭和清理连接失效的客户端
  • 如果是主服务器,需要定期对从服务器进行同步
  • 如果是集群模式,需要定期对集群进行同步和连接测试

1.3 事件调度

获取最近的时间事件的执行时间,根据当前时间判断还有多少时间remaindTime,然后阻塞remaindTime毫秒等待文件事件。如果文件事件到达,则先执行相应的文件事件,再执行到时间的时间事件;否则直接执行到时间的时间事件。
文件事件和时间事件都是同步、有序、原子的方式执行的,所以尽量减少阻塞时间,防止出现饥饿时间。因此耗时比较久的就会放到子进程或者子线程中执行,例如AOF重写;或者是像命令回复处理器将数据写入到套接字中,如果超过阈值就会等待下次写入。

注意:因为阻塞时间是最近的时间事件,先执行文件事件再执行时间事件,所以一般时间时间都会延迟,并不是准时执行。
在这里插入图片描述

2、Redis客户端

2.1 客户端的相关属性

针对于每个连接Redis服务器的客户端都会在连接应答处理器中创建一个Client结构对象。Client结构体定义如下:

typedef struct client {// 客户端基本信息uint64_t id;               // 客户端 IDint fd;                    // 文件描述符sds name;                  // 客户端名称int flags;                 // 客户端状态标志// 输入缓冲区sds querybuf;              // 输入缓冲区int argc;                  // 参数个数robj **argv;               // 参数数组struct redisCommand *cmd;  // 当前命令// 输出缓冲区char buf[PROTO_REPLY_CHUNK_BYTES]; // 固定大小的输出缓冲区list *reply;               // 动态分配的输出缓冲区// 数据库相关redisDb *db;               // 当前数据库int dictid;                // 数据库索引// 复制相关int replstate;             // 复制状态long long reploff;         // 复制偏移量// 阻塞与超时mstime_t bpop_timeout;     // 阻塞超时时间long long lastinteraction; // 上次交互时间// 统计信息long long query_start_time; // 命令开始执行时间size_t obuf_mem;           // 输出缓冲区内存占用// 其他int authenticated;         // 是否已认证connection *conn;          // 连接对象dict *pubsub_channels;     // 订阅的频道list *pubsub_patterns;     // 订阅的模式
} client;

在服务端的结构体中存放了所有连接的客户端状态的链表,可以通过遍历获取想要的客户端:

typedef struct redisServer {...list *clients;             // 客户端列表....
}

上述客户端的属性主要分为两部分:通用属性和特定属性【与特定的功能有关,例如订阅、事务】。这里介绍一些通用属性:

  • fd:套接字描述符。这个属性值是大于等于-1的整数。当客户端为伪客户端【命令来自于AOF载入或者lua脚本】的时候,fd=-1;其他情况的客户端都是大于-1.
  • name:客户端的名字。连接服务端的客户端一般是没有名字的,即这个字段为null。但是客户端可以使用client setname给自己设置一个名字,使得客户端的身份更加明确
  • flags:标志。记录了客户端的角色和目前所处的状态。例如REDIS_MULTI:客户端正在执行事务、REDIS_FORCE_AOF强制服务端将当前命令写入到AOF中
  • querybuf:输入缓冲区。记录客户端发送的命令请求。
  • argv、argc:参数数组。将命令请求存储入输入缓冲区之后,对命令请求进行解析,得到的命令参数和命令个数分别存储到argv和argc中。例如set key value命令,argv数组分别存储:“set” “key” “value”, argc=3。argv[0]是待执行的命令。
  • cmd:命令实现函数。redis中将所有的命令和命令实现函数存放到字典中。已知argv[0]即待执行的命令就可以得到该命令对应的实现函数。然后将得到的实现函数赋值给cmd属性。
  • 输出缓冲区:输出缓冲区分为两部分:
    • 缓冲区的大小是固定的【buf[PROTO_REPLY_CHUNK_BYTES]】,主要是存储一些长度比较小的回复。PROTO_REPLY_CHUNK_BYTES默认为16*1024,即buf数组为16KB。
    • 缓冲区的大小是变化的【reply】。存储一些比较大的复杂的命令回复,他是一个字符串链表。
  • authenticated:身份验证。表示客户端是否通过身份验证,通过验证authenticated=1,否则为0。如果身份验证不通过,在服务端开启身份验证的时候,客户端无法执行命令。

2.2 客户端的创建与关闭

不同类型的客户端,服务端对客户端的创建也略有不同。

2.2.1 普通客户端的创建

针对于通过网络连接的普通客户端,当客户端通过connect函数与redis服务器进行连接的时候,redis服务器就会利用文件事件分派器分配给连接应答处理器。连接应答处理器就会为这个客户端创建client对象,并且通过尾插法将这个client对象存储到redisServer的clients列表尾部。

2.2.2 普通客户端的关闭

客户端被关闭的情况比较多:

  • 客户端进程退出或者被杀死,那么客户端和服务端的网络连接也就被关闭了,从而就会导致服务端会关闭客户端
  • 客户端向服务端发送了不符合resp协议的命令
  • 客户端成为client kill的目标
  • 如果用户为服务端设置了timeout超时时间,如果客户端空转时间idle超过timeout,就会被关闭
  • 客户端发送的命令超过输入缓冲区,默认1G
  • 命令回复的内容超过输出缓冲区

2.2.3 AOF的伪客户端

redis服务器启动的时候,会创建伪客户端用于加载AOF文件,执行aof中的命令。等AOF文件载入结束就会关闭伪客户端。

2.2.4 Lua脚本的伪客户端

服务器在初始化的时候就会创建用来执行lua脚本中redis命令的伪客户端。这个伪客户端的目的就是充当lua脚本和redisServer的桥梁,用来执行lua脚本中的redis命令。例如

redis.call('SET', 'key', 'value')

redis.call() 会通过 lua_client 向 Redis Server 发送 SET 命令。这个伪客户端会随着服务器的关闭而关闭。

3、Redis服务端

Redis服务端负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存命令执行产生的数据,并根据资源管理来维持自身的运行。

接下来的执行过程的前提是已经创建好连接,即连接应答处理器执行完,创建好了Client对象。

3.1 命令请求的执行过程

3.1.1 客户端发送命令请求

客户端执行redis命令,然后客户端会将redis命令转换为resp协议格式,通过网络发送给redis服务端。以Jedis为例子:
在这里插入图片描述
底层源码:

java">public class Jedis extends BinaryJedis implements JedisCommands, MultiKeyCommands,AdvancedJedisCommands, ScriptingCommands, BasicCommands, ClusterCommands, SentinelCommands,ModuleCommands {protected final Client client;@Override// 执行redis命令public String set(final String key, final String value) {checkIsInMultiOrPipeline();client.set(key, value);return client.getStatusCodeReply();}
}public class Client extends BinaryClient implements Commands {@Overridepublic void set(final String key, final String value) {// 将命令转换为字节数组set(SafeEncoder.encode(key), SafeEncoder.encode(value));}public void set(final byte[] key, final byte[] value) {sendCommand(SET, key, value);}public void sendCommand(final ProtocolCommand cmd, final byte[]... args) {try {// 获取连接connect();// 转换为resp协议并且发送给客户端Protocol.sendCommand(outputStream, cmd, args);} catch (JedisConnectionException ex) {try {String errorMessage = Protocol.readErrorLineIfPossible(inputStream);if (errorMessage != null && errorMessage.length() > 0) {ex = new JedisConnectionException(errorMessage, ex.getCause());}} catch (Exception e) {}broken = true;throw ex;}}
}public final class Protocol {public static void sendCommand(final RedisOutputStream os, final ProtocolCommand command,final byte[]... args) {sendCommand(os, command.getRaw(), args);}private static void sendCommand(final RedisOutputStream os, final byte[] command,final byte[]... args) {try {// 这里转换为resp协议。ASTERISK_BYTE = '*';DOLLAR_BYTE = '$';os.write(ASTERISK_BYTE);os.writeIntCrLf(args.length + 1);os.write(DOLLAR_BYTE);os.writeIntCrLf(command.length);os.write(command);os.writeCrLf();for (final byte[] arg : args) {os.write(DOLLAR_BYTE);os.writeIntCrLf(arg.length);os.write(arg);os.writeCrLf();}} catch (IOException e) {throw new JedisConnectionException(e);}}}

3.1.2 读取命令请求

当客户端向Redis服务端发送了命令请求之后,套接字就会变得可读,服务端就会根据命令请求分派器调用命令请求处理器进行处理。命令请求处理器将会做如下操作:

  • 读取套接字中的符合resp协议的命令请求,并将其存储到相应client的输入缓冲区中
  • 解析输入缓冲区中的命令,分别提取出命令参数和命令个数存储到argv和argc属性中
  • 根据argv[0](存储的这个是命令,如set)去命令实现函数字典【命令表】中查找对应的实现函数【redisCommand对象】并赋值给Client对象的cmd属性,然后通过这个实现函数执行相应逻辑。

redisCommand结构体:

struct redisCommand {char *name;                      // 命令名称(如 "SET", "GET")redisCommandProc *proc;          // 命令的实现函数指针int arity;                       // 参数数量要求(负数表示可变参数)char *sflags;                    // 命令标志字符串(如 "w" 表示写命令),表示这个命令的一些特征:写命令、读命令、管理命令、发布订阅命令、可能会占用大量内存命令...uint64_t flags;                  // 命令标志位(由 sflags 转换而来)redisGetKeysProc *getkeys_proc;  // 获取键的函数指针(用于复杂命令)int firstkey;                    // 第一个键参数的位置int lastkey;                     // 最后一个键参数的位置int keystep;                     // 键参数之间的步长long long microseconds;          // 执行该命令的平均耗时(统计用)long long calls;                 // 该命令被调用的次数(统计用)
};

3.1.3 执行实现函数

上述说了通过命令表根据执行的命令名查找该命令的实现函数。在获取到实现函数之后,并不会立刻执行实现函数。

命令执行器的预备操作
在此之前,redisClient已经保存了实现函数(redisClient.cmd属性)、参数(redisClient.argv属性)、参数个数(redisClient.argc属性)。 在真正执行命令执行器【实现函数】之前还会有一些预备操作。包括但不限于:

  • cmd属性是不是为null,是不是没有找到相应的实现函数
  • 根据cmd.arity属性检查参数的个数是不是符合要求
  • 客户端是否通过了校验authenticated
  • 如果服务器打开了maxmemory功能,在执行之前先检查内存占用情况,并在需要的时候进行内存回收

命令执行器的执行环节
开始调用实现函数,将执行的结果存储到输出缓冲区中。

client->cmd->proc(client);

命令执行器的后续环节
执行完实现函数之后,还会存在一些后续操作:

  • 如果开启了满查询,判断当天查询是不是要增加一条满查询日志
  • 根据本次执行时间,更新该命令的平均耗时microseconds以及调用次数calls+1
  • 如果服务端开启了AOF持久化,需要将本命令写入到AOF缓冲区中
  • 如果有从服务器正在复制这个服务器,那么会将这个命令传递给从服务器

3.1.4 将命令回复发送给客户端

上述命令执行完会将执行结果存储到输出缓冲区中,命令请求处理器会将套接字的可写状态与命令回复处理器进行关联。当客户端可写时,就会执行命令回复执行器。命令回复执行器将输出缓冲区的数据发送给客户端,并且取消套接字可写状态与命令回复处理器的关联。
客户端会将服务端返回的resp协议结果转换为字符串结果。

3.2 serverCron函数

Redis的serverCron函数默认是100ms执行一次,进行管理服务器的资源。serverCron函数会执行如下操作:

struct redisServer{...// 保存了秒级精度的unix时间戳time_t unixtime;// 保存了毫秒级精度的unix时间戳long long mstime;// 当前的 LRU 时钟值,默认10s更新一次,用于计算idle空转时长unsigned int lruclock;  // 使用内存峰值size_t stat_peak_memory;// 服务器关闭表示。1:服务器关闭 0:服务器正常int shutdown_asap;// aof重写是否被延迟了,1:延迟int aof_rewrite_scheduled// 记录执行bgsave命令的子进程ID。-1:服务器没有执行bgsavepid_t rbd_child_pid;// 记录执行bgrewriteaof命令的子进程ID。-1:服务器没有执行bgrewriteaofpid_t aof_child_pid;// 存储serverCron时间事件执行次数int cronloops;...
}

更新服务器的时间缓存 :每100ms更新一次unixtime和mstime属性。因为很多操作都会获取系统的时间,而每次获取都进行系统调用,比较耗费性能。所以redis定期更新时间缓存unixtime和mstime属性,使用这两个属性代替系统时间。
这种适合精度要求不高的场景,例如:打印日志、是否执行持久化等。但是针对于判断键的过期时间、满查询等高精度时间就需要每次都进行系统调用。

更新LRU时钟:serverCron函数默认10s更新一次lruclock。每个redisObject都有一个lru属性用于计算上次访问时间,这个时间就是使用的lruclock。而计算一个对象的空转时间也是使用lruclock-redisObject.lru,因此空转时间idle这是一个估算时间。

更新服务器每秒钟命令执行次数:这个只是一个估计值。redisServer中有四个ops_sec_*开头的属性和其相关。根据上次计算时间和执行次数和当前的时间和次数,计算平均一分钟执行的个数。同时还有一个数组保存了过去16次的平均执行次数,而获取的每秒命令执行次数是过去16个平均值的平均。

更新内存使用峰值记录:每次执行serverCron函数都会查看服务器使用的内存数量,然后和stat_peak_memory进行比较,较大的存储到stat_peak_memory中。

处理sigterm信号:服务器在启动的时候会为SIGTERM信号关联一个信号处理器sigtermHandler,即如果服务器接收到SIGTERM信号,sigtermHandler函数就会将shutdown_asap置为1. serverCron每次执行的时候都会检查shutdown_asap值。

管理客户端资源:serverCron函数每次执行都会调用clientsCron函数,clientsCron函数会进行如下操作:

  • 检查客户端和服务端的连接是否已经超时
  • 检查上次命令之后输入缓冲区是否超过一定长度【每次命令都会追加到缓冲区中】,如果超过会释放当天输入缓冲区,重新创建一个新的

管理服务端资源:serverCron函数每次执行都会调用databasesCron函数,databasesCron函数会对数据库进行检查,删除一些过期键,适当的时候还会对字典进行伸缩操作。

执行被延迟的BGREWRITEAOF: 当服务端在执行bgsave命令的时候,如果客户端发送了bgrewriteaof命令,那么aof重写会被延迟到bgsave命令执行之后,但是会设置aof_rewrite_scheduled=1。在执行serverCron函数的时候,会检查当前是不是在执行bgsave或者bgrewriteaof,如果没有并且aof_rewrite_scheduled=1,那么服务端就会执行被延迟的aof重写。

检查持久化运行状态:serverCron函数会检查rbd_child_pid和aof_child_pid是不是都为-1。是否有一个不为-1:

  • 则等待一会。在等待期间如果有信号从子进程返回过来,说明rdb持久化或者aof重写完成了,则执行文件替换。将新生成的文件【rdb文件或者aof重写文件】替换原来文件
  • 如果都是-1,则判断有没有延迟的aof重写,如果有则执行;没有在判断配置的自动持久化是不是满足了,如果满足则进行bgsave;否则判断AOF重写条件是不是满足。
    在这里插入图片描述
    将AOF缓存中的数据持久化到AOF文件:如果服务端开启了aof持久化,那么aof缓存中会有待写入的数据,serverCron函数会调用相应的程序进行持久化。

关闭异步客户端:关闭那些超过输出缓冲区超过硬限制的客户端。

增加cronloops次数:每执行一次serverCron函数,cronloops都会加1。

3.3 初始化服务器

上述的命令请求和serverCron运行都是在服务器已经启动的前提下,本小节就是介绍服务器启动的时候的一些流程和初始化。

redisServer_348">3.3.1 初始化服务器的状态结构redisServer

初始化服务器的第一步肯定是创建redisServer对象。然后就会对redisServer对象中的属性进行初始化。初始化第一步就是先给部分属性【除了命令表这种创建了结构对象,大部分都是整数、浮点数、字符串这种基本类型】赋默认值。

  • 服务器的运行ID
  • 服务器的默认运行频率
  • 服务器的默认文件路径
  • 服务器的运行架构
  • 服务器的默认端口号
  • 服务器的默认RDB、AOF持久化条件
  • 服务器的LRU时钟
  • 创建命令表

这些都是在initServerConfig函数中实现的默认初始化。

3.3.2 载入配置选项

在初始化默认的配置之后,然后根据配置文件中的相关配置或者命令参数修改、更新配置参数。

3.3.3 初始化服务器中的数据结构

上述只是初始化一些简单的配置,除了命令表。到这就开始初始化一些复杂的属性,通过initServer函数执行。

  • 初始化server.clients链表,用来存储客户端的redisClient连接对象
  • 创建server.db数据库
  • 创建用来保存订阅信息和订阅模式的字典和列表
  • 创建共享对象,主要是一些redis比较常用值,例如"ok"的字符串、0-10000的字符串
  • 监听端口,并且为套接字关联连接应答处理器
  • 为serverCron创建时间事件
  • 如果打开了AOF持久化,打开已有的AOF文件,没有AOF文件就新建

3.3.4 还原数据库状态

服务器初始化完成之后,就要根据RDB文件或者AOF文件还原数据库状态。如果服务器开启了AOF持久化,则优先使用AFO文件进行还原;否则使用RDB进行还原。因为AOF文件实时性更高,丢失数据的概率更小。

3.3.5 执行事件循环

开始执行事件循环,执行文件事件和时间事件。至此服务器可以接受客户端的请求。


http://www.ppmy.cn/news/1582960.html

相关文章

告别流媒体会员!如何用Docker搭建可远程控制的家庭音乐服务器

文章目录 前言1. 添加镜像源2. 本地部署Melody3. 本地访问与使用演示4. 安装内网穿透5. 配置Melody公网地址6. 配置固定公网地址 前言 嗨,各位音乐发烧友们!今天我要带你们解锁一个超酷的新技能——在香橙派Zero3上搭建自己的在线音乐平台,并…

langgraph简单Demo3(画一个简单的图)

文章目录 画图简单解析再贴结果图 画图 from langgraph.graph import StateGraph, END from typing import TypedDict# 定义状态结构 # (刚入门可能不理解这是什么,可以理解为一个自定义的变量库,你的所有的入参出参都可以定义在这里) # 如下&#xff1…

免费提供多样风格手机壁纸及自动更换功能的软件

在寻找手机壁纸时,要是能有一款软件,既免费又资源丰富,操作还简单方便,那可就太棒了。今天就给大家介绍这样一款软件——壁纸喵,它能满足你对手机壁纸的各种需求。 壁纸喵是一款完全免费的手机壁纸软件,其…

朴素贝叶斯:文本处理中的分类利器

在大数据与人工智能时代,文本处理任务无处不在,如垃圾邮件分类、用户情感预测等。朴素贝叶斯算法凭借简洁的原理和高效的计算,成为文本处理领域的经典方法。它如何在这些场景中发挥作用?让我们深入探索。 垃圾邮件分类 假设我们…

【性能优化点滴】odygrd/quill 中的冷热属性宏

以下是对这段代码的详细解析: 代码功能概述 这段代码定义了三个 GCC/Clang 特有的编译器属性宏,用于指导编译器进行优化: QUILL_ATTRIBUTE_HOT:标记高频执行的 “热” 函数QUILL_ATTRIBUTE_COLD:标记低频执行的 “冷…

pyside6的QGraphicsView体系,当鼠标位于不同的物体,显示不同的右键菜单

代码: # 设置样本图片的QGraphicsView模型 from PySide6.QtCore import Qt, QRectF, QObject from PySide6.QtGui import QPainter, QPen, QColor, QAction, QMouseEvent from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGra…

智能语音交互指路牌:未来城市导航的革新力量

在东京涩谷的十字路口,一位外国游客正盯着密密麻麻的日文路牌不知所措。当他靠近一块银色金属立柱时,顶端的蓝色指示灯突然亮起:”您好,需要帮助吗?我可以提供中英文导航服务。” 这并非科幻电影场景,而是全…

Python 网页爬取入门指南

随着互联网数据的不断增长,网页爬取(Web Scraping)成为数据采集与分析的重要手段。Python 作为一门简单高效的编程语言,提供了丰富的第三方库,能够帮助开发者快速构建爬虫。本文将带您了解网页爬取的基本概念&#xff…