Redis设计与实现(第二部分):单机数据库的实现

本文详细介绍了Redis数据库的内部结构,包括服务器中的数据库结构、切换数据库、键空间以及数据库操作。重点讨论了键的生存时间和过期策略,如定时删除、惰性删除和定期删除,并解析了Redis如何在AOF、RDB和复制中处理过期键。此外,还概述了RDB持久化和AOF持久化的工作原理,包括文件事件处理器和时间事件在Redis中的角色。最后,提到了客户端的创建与关闭以及服务器如何处理命令请求。
摘要由CSDN通过智能技术生成

Redis设计与实现(第二部分):单机数据库的实现

8. 数据库

8.1 服务器中的数据库

  Redis服务器将数据库保存在 redisServer 结构的db数组中,db数组的每个项都是一个 redisDb 结构,每个redisDb结构代表一个数据库,redisServer中有一个 dbnum 属性,它决定服务器初始化时创建多少个数据库,默认值为16.

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8.2 切换数据库

  Redis客户端默认目标数据库为0号数据库,可以通过 select 命令切换,redisClient结构的db属性 记录了客户端当前的目标数据库

在这里插入图片描述

8.3 数据库键空间

  redisDb结构的dict字典 保存了数据库中的所有键值对,我们将这个字典称为键空间

在这里插入图片描述

  键空间的键就是数据库的键,是一个字符串

  键空间的值也就是数据库的值,可以是Redis任意一种对象

  键空间是一个字典,所以针对数据库的操作,实际上都是在操作这个字典

添加新键:给字典添加新的键值对

删除键:删除对应的键值对对象

更新键:更新键所对应的值

对键取值:取出键对应的值

注意:当使用Redis命令对数据库进行读写时,服务器不仅执行读写操作,还会执行额外的维护操作,如更新命中次数,更新LRU等。

8.4 设置键的生存时间或过期时间

  通过 EXPIRE 命令或者 PEXPIRE 命令,可以为数据库中的某个键设置生存时间,到期这个键就会被自动删除,TTLPTTL 命令可返回这个键的剩余生存时间。

  Redis有四个命令设置生存/过期时间,但最终都会转换成 PEXPIREAT 执行。
在这里插入图片描述

  redisDb结构的 expires字典 保存了数据库中所有键的过期时间,这个过期字典为一个指针,键指向某个键对象,值为一个long long 整数,保存过期时间

  检查键是否过期:检查该键是否存在过期字典 --> 存在获取过期时间 --> 和当前时间比较。

8.5 过期键删除策略

  三种:定时删除、惰性删除、定期删除

定时删除

设置过期时间的同时,创建一个定时器,到期立即删除

对内存友好,会尽快释放内存,但是对CPU时间不友好,在内存不紧张CPU时间紧张时,影响服务器相应时间和吞吐量。

并且创建定时器需要用到时间事件,时间事件使用无序链表实现,查找事件的时间复杂度为O(N),创建大量定时器效率低。

惰性删除

不刻意去管,但每次获取键时都检查是否过期,过期了就删除

对CPU时间友好,但是对内存不友好,过期的键只要不访问就永远不会删除,这样的键多了的话会造成内存泄露。

定期删除

每隔一段时间,就检查删除一次,删除多少由算法决定

对上边两种策略的折中,但难点在于确定删除操作执行的时长和频率。
8.6 Redis的过期键删除策略

  过期键的惰性删除策略由 expireIfNeeded 函数实现,expireIfNeeded函数就像一个过滤器,所以读写数据的命令执行之前都会调用它,过期就删除键返回空。

在这里插入图片描述

  过期键的定期删除策略由 activeExpireCycle 函数实现

  这个函数每次运行,都会随机取出一定数量的键检查,并删除过期的键,然后由全局变量current_db记录进度,所有数据库都被检查一遍后current_db置0,重新开始

8.7 AOF、RDB和复制功能对过期键的处理
8.7.1 生成和载入RDB文件

生成

创建新RDB文件时,程序会对数据库中的键进行检查,过期键不会被保存到RDB文件

所以,数据库中包含过期键不会对生成新的RDB文件造成影响。

载入

如果以主服务器模式运行,载入时会对文件中保存的键进行检查并忽略过期键,所以不会造成影响。

如果以从服务器模式运行,所以的键都会被载入,但是主从同步时,从数据库会被清空,所以也不影响。
8.7.2 AOF文件写入和重写

写入

过期键不会对AOF文件产生任何影响,当过期键被删除后,程序会向AOF文件追加一条DEL命令,来显式地记录该键已被删除。

重写

和生成RDB文件时类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。
8.7.3 复制
从服务器的过期键删除动作统一由主服务器控制,主服务器删除过期键后,显式的向所有从服务器发送DEL命令告知删除

这样可以保证主从数据库的一致性

9. RDB持久化

  Redis将自己的数据库 状态(非空数据库的键值对) 存储在内存中,这样一旦服务器退出,数据就会消失,所以为了避免数据意外丢失,就需要将数据持久化到磁盘中。

9.1 RDB文件的创建与载入

生成RDB文件的两个命令:SAVE、BGSAVE

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

BGSAVE:派生出一个子进程,由子进程负责创建RDB文件,服务器进程继续处理命令请求

RDB文件的载入工作是在服务器启动时自动执行的

注:如果开启了AOF持久化,则优先使用AOF还原数据库状态,AOF关闭时,才使用RDB。

载入RDB文件期间,服务器会处于阻塞状态,直到载入完成。
9.2 自动间隔性保存

Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令

可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令,如:

save 900 1
save 300 10
save 60 10000
9.2.1 设置保存条件

  Redis启动时,可以指定配置文件或者传入启动参数设置save选项,不设置就使用默认条件,然后会将这些条件保存到 redisServer结构的saveparams属性

在这里插入图片描述

9.2.2 dirty计数器和lastsave属性
dirty计数器:记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态进行了多少次修改

lastsave:记录了服务器上一次成功执行SAVE命令或者BGSAVE命令的时间
9.3 RDB文件结构

在这里插入图片描述

10. AOF持久化

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

  被写入AOF文件的所有命令都是以Redis的 命令请求协议格式 (纯文本格式)保存的

10.1 AOF持久化的实现

三步:为命令追加、文件写入、文件同步

命令追加

当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾

文件写入与同步

Redis的服务器进程就是一个事件循环

循环中文件事件负责接收客户端的命令请求,以及向客户端发送命令回复

循环中而时间事件负责定时运行的一些函数

处理文件事件时执行的写命令,会被追加到aof_buf缓冲区

服务器每次结束一个事件循环之前,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面
10.2 AOF文件的载入与数据还原

  因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态,如下

在这里插入图片描述

10.3 AOF重写

  随着时间的推移,AOF文件中的内容会越来越多,体积越来越大,还原所需要的时间也就越多,并且会对服务器造成不好的影响,为了解决AOF文件膨胀问题,Redis提供了AOF文件重写功能

10.3.1 AOF文件重写的实现

  AOF文件重写是通过读取服务器当前的数据库状态来实现的,因此并不需要对现有的AOF文件进行读取等操作

实现

从数据库中读取键现在的值

用一条命令去记录键值对,代替之前记录这个键值对的多条命令

从而减少命令的冗余,减小文件体积
10.3.2 AOF后台重写

  因为Redis服务器使用单个线程来处理命令请求,所以如果由服务器直接调用 aof_rewrite 函数的话,那么在重写AOF文件期间,服务期将无法处理客户端发来的命令请求,所以,Redis决定将AOF重写程序放到子进程里执行,但子进程在进行AOF重写期间,可能会对现有的数据库状态进行修改,导致数据库状态不一致问题

解决

Redis服务器设置了一个AOF重写缓冲区,创建子进程后开始使用

Redis执行一个写命令,同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区

当子进程完成AOF重写工作之后,发信号给父进程

父进程将AOF重写缓冲区中的所有内容写入到新AOF文件

对新的AOF文件进行改名,覆盖现有AOF文件

11. 事件

11.1 文件事件

  Redis基于Reactor模式开发了自己的网络事件处理器:文件事件处理器

文件事件处理器使用 I/O多路复用程序同时监听多个套接字,并根据套接字的当前任务关联不同的事件处理器

当被监听的套接字准备执行操作时,就会产生相应的文件,然后文件处理器就会调用关联的事件处理器处理事件
文件事件处理器的构成

四个部分:套接字、I/O多路复用程序、文件事件分派器,以及事件处理器

在这里插入图片描述

文件事件是对套接字操作的抽象

I/O多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字

多个文件事件会并发的出现,I/O多路复用程序会将所以产生事件的套接字放到一个对列里边,并有序、同步的传给分派器

事件分派器根据套接字的事件类型,调用对应事件处理器
客户端与服务器连接

在这里插入图片描述

11.2 时间事件

Redis时间事件分为 定时事件周期性事件

定时事件:让某段程序一段时间后执行一次

周期性事件:让某段程序每隔一段时间就执行一次

一个时间事件主要由以下三个属性组成:

id:全局唯一ID

when:记录事件到达时间的时间戳

timeProc:时间事件处理器,一个函数

实现

服务器将所有时间事件都放在一个无序链表中

每当时间事件执行器运行时,它就遍历整个链表

查找所有已到达的时间事件,并调用相应的事件处理器

新的时间事件总是插入到链表的表头,该链表不按when属性的大小排序,所以要确保已达事件可以全部处理到,必须遍历所有事件

文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,并且处理事件的过程中也不会进行抢占

12. 客户端

  一个Redis服务器可以与多个客户端建立网络连接,虽然Redis使用单线程单进程的方式来处理命令请求,但是通过使用由I/O多路复用技术实现的文件事件处理器,使其能够与多个客户端进行网络通信。

  每个与服务器连接的客户端,服务器都会为这其建立对应的 redisClient 结构,保存客户端状态信息。

  Redis服务器结构里边有一个属性 clients ,是一个链表,保存所有与服务器连接的客户端。

12.1 客户端属性

看注释

typedef struct redisClient {

    // 套接字描述符
    int fd;

    // 当前正在使用的数据库
    redisDb *db;

    // 当前正在使用的数据库的 id (号码)
    int dictid;

    // 客户端的名字
    robj *name;

    // 输入缓冲区:保存客户端发送的命令请求
    sds querybuf;

    // 查询缓冲区长度峰值
    size_t querybuf_peak;

    // querybuf中命令参数数量
    int argc;

    // querybuf中命令参数对象数组
    robj **argv;

    // 记录被客户端执行的命令
    struct redisCommand *cmd, *lastcmd;

    // 请求的类型:内联命令还是多条命令
    int reqtype;

    // 剩余未读取的命令内容数量
    int multibulklen;

    // 命令内容的长度
    long bulklen;

    // 回复链表
    list *reply;

    // 回复链表中对象的总大小
    unsigned long reply_bytes;

    // 已发送字节,处理 short write 用
    int sentlen; 

    // 创建客户端的时间
    time_t ctime; 

    // 客户端最后一次和服务器互动的时间
    time_t lastinteraction; 

    // 客户端的输出缓冲区超过软性限制的时间
    time_t obuf_soft_limit_reached_time;

    // 客户端状态标志
    int flags; 

    // 当 server.requirepass 不为 NULL 时
    // 代表认证的状态
    // 0 代表未认证, 1 代表已认证
    int authenticated; 

    // 复制状态
    int replstate;
    
    // 用于保存主服务器传来的 RDB 文件的文件描述符
    int repldbfd;

    // 读取主服务器传来的 RDB 文件的偏移量
    off_t repldboff;
    
    // 主服务器传来的 RDB 文件的大小
    off_t repldbsize; 
    
    sds replpreamble;  

    // 主服务器的复制偏移量
    long long reploff;    
    // 从服务器最后一次发送 REPLCONF ACK 时的偏移量
    long long repl_ack_off; 
    // 从服务器最后一次发送 REPLCONF ACK 的时间
    long long repl_ack_time;
    // 主服务器的 master run ID
    // 保存在客户端,用于执行部分重同步
    char replrunid[REDIS_RUN_ID_SIZE+1]; 
    // 从服务器的监听端口号
    int slave_listening_port; 

    // 事务状态
    multiState mstate;     

    // 阻塞类型
    int btype;              
    // 阻塞状态
    blockingState bpop;    
    // 最后被写入的全局复制偏移量
    long long woff;        

    // 被监视的键
    list *watched_keys;    

    // 这个字典记录了客户端所有订阅的频道
    // 键为频道名字,值为 NULL
    // 也即是,一个频道的集合
    dict *pubsub_channels; 

    // 链表,包含多个 pubsubPattern 结构
    // 记录了所有订阅频道的客户端的信息
    // 新 pubsubPattern 结构总是被添加到表尾
    list *pubsub_patterns;  
    sds peerid;            

    // 回复偏移量
    int bufpos;
    // 回复缓冲区
    char buf[REDIS_REPLY_CHUNK_BYTES];

} redisClient;
12.2 客户端的创建与关闭

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

在这里插入图片描述
  客户端被关闭的原因:网络连接关闭、发送了不合协议格式的命令请求、成为CLIENT KILL命令的目标、空转时间超时、输出缓冲区的大小超出限制等。

  处理Lua脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,直到服务器关闭。

  载入AOF文件时使用的伪客户端在载入工作开始时动态创建,载入工作完毕之后关闭。

13. 服务器

13.1 命令请求的执行过程

以 SET KEY VALUE

1)客户端向服务器发送命令请求SET KEY VALUE。
	
	客户端将请求转换成协议格式,通过连接到服务器的套接字,将请求发送给服务器

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

	读取套接字中命令请求,保存到客户端状态的输入缓冲区
	
	分析命令,提取命令参数和个数保存

	调用执行器执行

3)服务器将命令回复OK发送给客户端。

4)客户端接收服务器返回的命令回复OK,并将这个回复打印给用户观看。
13.2 serverCron函数

  Redis服务器中的serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转。

工作:

更新服务器时间缓存

更新LRU时钟

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

更新服务器内存峰值记录

处理SIGTERM信号

管理客户端资源

管理数据库资源

执行被延迟的BGREWRITEAOF

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

将AOF缓冲区中的内容写入AOF文件

关闭异步客户端

增加cronloops计数器的值
13.3 服务器初始化

服务器从启动到能够处理客户端的命令请求需要执行以下步骤

1)初始化服务器状态;

2)载入服务器配置;

3)初始化服务器数据结构;

4)还原数据库状态;

5)执行事件循环。

OVER(∩_∩)O~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Anton丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值