9-14章---单机数据库的实现

9、数据库

  • select实现原理:通过修改RedisClient.db指针,让他指向服务器中的不同数据库,从而实现切换目标数据库的功能
  • typedef struct redisDb{
    dict *dict;  //键空间
       dict *expires;  //过期键,是一个long long类型的整数,保存了键所指向的数据库键的过期时间
    }
  • 键空间

    • 键空间的键也就是数据库的键,每个键都是一个字符串对象。数据库的键空间是一个字典

    • 键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的任意一种Redis对象。

  • 添加新键:将一个新键值对添加到键空间字典中
  • 删除键:就是将键值对从键空间中删除
  • 读写键空间时的维护操作

    • 在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中( hit)次数或键空间不命中( miss)次数

    • 在读取一个键之后,服务器会更新键的LRU(最后一次使用)时间,这个值可以用于计算键的闲置时间,使用OBJECT idletime <key>命令可以查看键key的闲置时间。

    • 如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键

    • 服务器每次修改一个键之后,都会对脏( dirty)键计数器的值增1,这个计数器会触发服务器的持久化以及复制操作

    • 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知

  • 设置键的生存时间和过期时间

    • PERSIST:可以在过期字典中查找过期的键,并解除键和过期时间的关联

    • TTL:以秒为单位返回返回键的剩余时间,PTTL是以毫秒为单位
  • 过期键的删除策略

    • 定时删除

      • 优点:到期就删除过期键,释放了内存

      • 缺点:如果过期键的数量挺多,那么删除的时候就会占用CPU较长时间,会对服务器的响应时间和吞吐量造成影响

    • 惰性删除:只有在取出键时才对过期与否进行检查

      • 缺点:对内存不友好

    • 定期删除

      • 定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的内存浪费。

      • 定期删除策略的难点是确定删除操作执行的时长和频率

  • Redis的过期键删除策略

    • 使用的是惰性删除和定期删除

  • RDB对过期键的处理

    • 执行SAVE命令或者BGSAVE命令时会生成RDB文件,会把不过期的键复制过去

    • 如果服务器以主服务器模式运行:载入RDB文件时,只载入不过期键

    • 如果以服务器模式运行:会载入所有的键

  • AOF对过期键的处理

    • 当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF 文件不会因为这个过期键而产生任何影响。

    • 当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加( append)一条DEL命令,来显式地记录该键已被删除。

    • 和生成RDB文件时类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。

  • 复制模式对过期键的处理

    • 从服务器的过期键删除动作由主服务器控制

    • 从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键

    • 通过主服务器控制从服务器删除过期键,保证了主从服务器数据的一致性

  • 当Redis命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。

10、RDB持久化

  • 因为Redis是内存数据库,它将自己的数据库状态储存在内存里面,所以如果不想办法将储存在内存中的数据库状态保存到磁盘上,那么一旦服务器程序退出,服务器中的数据库状态也会消失,所以进入了RDB持久化。RDB文件是一个压缩的二进制文件,保存在硬盘上

  • RDB文件的创建和载入

    • SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止

    • BGSAVE命令会创建一个子进程,由子进程去创建RDB文件

    • RDB文件的载入工作是在服务器启动时自动执行的,载入期间会一直处于阻塞状态

    • 如果开启了AOF功能,会优先使用AOF文件恢复还原数据库状态

    • 当使用SAVE命令时,服务器会拒绝接收任何客户端的指令

    • 当使用BGSAVE时,客户端发送的SAVE、BGSAVE命令会被服务器拒绝,BGREWRITEAT命令延迟到BGSAVE命令执行完再执行

  • 自动间隔性保存

    • 因为BGSAVE命令可以在不阻塞服务器进程的情况下执行,所以Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。redis服务器会周期性的执行函数看是否满足save选项

  • RDB文件结构

    • db_version 4字节,代表RDB文件的版本号

    • databases部分包含着零个或任意多个数据库,以及各个数据库中的键值对数据

    • EOF常量的长度为1字节,这个常量标志着RDB文件正文内容的结束

    • check_sum是一个8字节长的无符号整数,保存着一个校验和,这个校验和是程序通过对REDIs、db_version、databases、EOF四个部分的内容进行计算得出的。

    • 在载入RDB文件时,会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现。

  • 每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、key_value_pairs三个部分

    • db_number保存着一个数据厍号码,根据号码的大小不同,这个部分的长度可以是1字节、2字节或者5字节。当程序读入db_number部分之后,服务器会调用SELECT命令,根据读入的数据库号码进行数据库切换,使得之后读入的键值对可以载入到正确的数据库中。

    • key_value_pairs部分保存了数据库中的所有键值对数据,如果键值对带有过期时间,那么过期时间也会和键值对保存在一起。根据键值对的数量、类型、内容以及是否有过期时间等条件的同,key_value_pairs部分的长度也会有所不同。

  • 键值对在RDB文件中的表示形式

    • TYPE、key、value三部分组成

  • 带有过期时间的键值对保存形式

  • 分析RDB文件

    • 使用od命令分析,该命令可以用给定的格式转存并打印输入文件

11、AOF持久化

  • RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的

  • AOF持久化的实现

    • 实现可分为命令追加(append)、文件写入、文件同步(sync)三个步骤

    • 命令追加

      • 服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾

    • 文件的写入和同步

      • 服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到缓冲区里,所以在每次结束一个事件循环之前,会考虑是否将缓冲区的内容写入到AOF文件里。

      • 为了提高文件的写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正地将缓冲区中的数据写入到磁盘里面。这种做法存在安全问题。因此操作系统可以强制数据读出磁盘

  • AOF文件的载入和还原

  • AOF重写(BGREWRITEAT原理)

    • 为了解决AOF文件体积膨胀的问题,AOF文件重写( rewrite)功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF 文件所保存的数据库状态相同,但新AOF 文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多。

    • 实现:不是读取现有AOF文件,而是根据现有数据库状态,用最少的命令去得到这个状态。

    • redis把AOF重写任务放到子进程中处理。不过,使用子进程也有一个问题需要解决,因为子进程在进行AOF重写期间,服务器进程还需要继续处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。

      • 为了解决不一致性的问题:redis服务器设置了AOF重写缓冲区,缓冲区在服务器创建子进程的时候使用,当Redis服务器执行完一个写命令之后,它会同时将个写命令发送给AOF缓冲区和AOF重写缓冲区,

    • 当子进程完成重写操作时,会向父进程发送一个信号,父进程会执行信号处理函数:

      • 将AOF重写缓冲区中的所有内容写人到新AOF 文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。

      • 对新的AOF文件进行改名,原子地( atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换。

12、事件

  • 文件事件:Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。

    • 文件事件处理器使用I/O多路复用( multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。

    • 事件的类型:AE_READABLE和AE_WRITABLE,当一个套接字即可读也可改时,优先执行读

  • 时间事件:Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。

    • 分为 定时事件和周期事件(只使用)

    • 由三个属性组成:id 、when、时间事件处理器函数

    • 实现:服务器将所有时间事件都放在一个无序链表(链表不按照when大小排序)中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

    • 持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作由redis.c/serverCron函数负责执行

  • 文件事件处理器的构成

    • IO多路复用程序负责监听多个套接字(套接字放在队列中),并向文件事件分派器传送那些产生了事件的套接字。

    • 文件事件分派器接收IO多路复用程序传来的套接字,并根据套接字产生的事件的类型,调用相应的事件处理器。

    • 服务器会为执行不同任务的套接字关联不同的事件处理器,这些处理器是一个个函数,它们定义了某个事件发生时,服务器应该执行的动作。

  • 文件事件的处理器

    • 连接应答处理器----accept

    • 命令请求处理器----read

    • 命令回复处理器----write

  • redis中文件事件和时间事件同时存在,他们的调度方式如下:

  • 对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占,因此,不管是文件事件的处理器,还是时间事件的处理器,它们都会尽可地减少程序的阻塞时间,并在有需要时主动让出执行权,从而降低造成事件饥饿的可能性。

13、客户端

  • Redis服务器状态结构的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构,对客户端执行批量操作,或者查找某个指定的客户端,都可以通过遍历链表来完成

  • 客户端的通用属性

    • struct redisClient{
         int fd;
         robj* name;
         int falgs;
         sds querybuf; //输入缓冲区
         
         rogb** argv;//argv[0]:命令,以后是命令参数
         int argc;  //argv数组的长度
         
         struct redisCommend* cmd;
         
         char buf[REDIS_REPLY_CHUNK_BYTES];//默认大小是16KB
         int bufpos;  //记录了buf已使用的大小
         list *reply;  //链表组成了可变缓冲区
         int authenticated;
         
         time_t ctime;  //客户端创建的时间
      time_t lastinteraction;   //客户端与服务器最后一次互动的时间
      time_t obuf_soft_limit_reached_time ;  //输出缓冲区第一次达到了软性限制的时间

      }
    • 套接字描述符:客户端状态的fd属性记录了客户端正在使用的套接字描述符

      • 伪客户端( fake client)的fd属性的值为-1:伪客户端处理的命令请求来源于AOF文件或者Lua脚本,目前Redis服务器会在两个地方用到伪客户端,一个用于载入AOF文件并还原数据库状态,而另一个则用于执行Lua脚本中包含的Redis命令。

      • 普通客户端的fd属性的值为大于-1的整数:普通客户端使用套接字来与服务器进行通信,所以服务器会用fd属性来记录客户端套接字的描述符。

    • 名字:一般情况下,连接到服务器的客户端都是没有名字的

    • 标志:记录了客户端目前所处的状态和角色

    • 输入缓冲区:用于保存客户端发送的命令请求,会根据内容的大小进行缩小与扩大,但不会超过1G

    • 命令与命令参数:在服务器将客户端发送的命令请求保存到客户端状态的querybuf属性之后,服务器将对命令请求的内容进行分析,并将得出的命令参数以及命令参数的个数分别保存到客户端状态的argv属性和argc属性

    • 命令的实现函数:当服务器从协议内容中分析并得出argv属性和argc属性的值之后,服务器将根据项argv [0]的值,在命令表中查找命令所对应的命令实现函数。

    • 输出缓冲区:执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面,每个客户端都有两个输出缓冲区可用,一个是固定的,一个是可变的

      • 固定大小的缓冲区用于保存那些长度比较小的回复,比如 OK、简短的字符串值、整数值、错误回复等等。当固定的用完之后,服务器就会用可变大小的缓冲区

      • 可变大小的缓冲区用于保存那些长度比较大的回复,比如一个非常长的字符串值,一个由很多项组成的列表,一个包含了很多元素的集合等等。

    • 身份验证:客户端状态的authenticated属性用于记录客户端是否通过了身份验证

      • 0:未通过身份验证;1:通过了

    • 时间

  • 客户端的创建与关闭

    • 创建普通客户端:如果客户端是通过网络连接与服务器进行连接的普通客户端,那么在客户端使用connect函数连接到服务器时,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构clients链表的末尾。

    • 关闭普通客户端:关闭的原因有很多中,,,,

      • 为了避免客户端的回复过大,占用过多的服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作。包括硬件限制和软件限制

    • 服务器会在初始化时创建负责执行Lua脚本中包含的Redis命令的伪客户端,并将这个伪客户端关联在服务器状态结构的lua_client属性中

    • 服务器在载人AOF文件时,会创建用于执行AOF文件包含的Redis命令的伪客户端,并在载入完成之后,关闭这个伪客户端。

14、服务器

  • 命令请求的过程:以SET KEY VALUE为例:

    • 客户端向服务器发送命令请求 SET KEY VALUE。

    • 服务器接收并处理客户端发来的命令请求SET KEY VALUE,在数据库中进行设置操作,并产生命令回复OK。

    • 服务器将命令回复oK发送给客户端。

    • 客户端接收服务器返回的命令回复OK,并将这个回复打印给用户观看。

  • 具体的过程如下:

    • 发送命令请求:会将这个命令请求转化成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器

    • 读取命令请求:当客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:

      • 读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面。

      • 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的argv属性和argc属性里面。

      • 调用命令执行器,执行客户端指定的命令。

  • 命令执行器(1):查找命令实现

    • 命令执行器要做的第一件事就是根据客户端状态的argv[0]参数,在命令表( commandtable)中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性里面。

  • 命令执行器(2):预备操作

    • 到目前为止,服务器已经将执行命令所需的命令实现函数(保存在客户端状态的cmd属性)、参数(保存在客户端状态的argv属性)、参数个数(保存在客户端状态的argc属性)都收集齐了,但是在真正执行命令之前,程序还需要进行一些预备操作,从而确保命令可以正确、顺利地被执行,这些操作包括:

      • 检查客户端状态的cmd指针是否指向NULL

      • 根据客户端cmd属性指向的rediscommand结构的arity属性,检查命令请求所给定的参数个数是否正确,当参数个数不正确时,不再执行后续步骤

      • 检查客户端是否通过了身份验证

      • 如果服务器打开了maxmemory功能,那么在执行命令之前,先检查服务器的内存占用情况,并在有需要时进行内存回收,从而使得接下来的命令可以顺利执行。

      • 。。。。。。

  • 命令执行器(3):调用命令的实现函数

    • 在前面的操作中,服务器已经将要执行命令的实现保存到了客户端状态的cmd属性里面,并将命令的参数和参数个数分别保存到了客户端状态的argv属性和argv属性里面,当服务器决定要执行命令时,它只要执行以下语句就可以了:client->cmd->proc(client),这些回复会被保存在客户端状态的输出缓冲区里面

  • 命令执行器(4):执行后续操作

    • 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。

    • 根据刚刚执行命令所耗费的时长,更新被执行命令的redisCommand结构的milliseconds属性,并将命令的rediscommand结构的calls计数器的值增一

    • 如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚刚执行的命令请求写入到AOF缓冲区里面。

    • 如果有其他从服务器正在复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有从服务器。

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

  • 客户端收到并打印命令回复

    • 当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人类可读的格式,并打印给用户观看(假设我们使用的是 Redis自带的redis-cli客户端)

serverCron函数介绍:默认每隔100ms执行一次,这个函数负责管理服务器的资源,并保证服务器能够正常运转

  • 更新服务器时间缓存

  • 更新LRU时钟

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

  • 更新服务器内存峰值记录

  • 处理SIGTERM信号

  • 管理客户端资源

  • 管理数据库资源

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

  • 将AOF缓冲区的内容写到AOF文件

  • 关闭异步客户端

初始化服务器

  • 初始化服务器状态结构:创建 struct redisServer类型的server变量保存服务器的状态,并为各个属性设置默认值

  • 载入配置选项:在启动服务器时,用户可以通过给定配置参数或者指定配置文件来修改服务器的默认配置。

  • 初始化服务器数据结构

  • 还原数据库状态:在完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。

  • 执行事件循环:

posted @ 2022-04-14 21:41  Z9Y1J5  阅读(29)  评论(0编辑  收藏  举报