Redis 单机数据库

发布于 2022-01-13 12:48:46 字数 10093 浏览 1046 评论 0

一个数据库里用一个字典保存了键值对,称为键空间。键空间的键是一个字符串对象,值是上面五种redis对象之一。

键的过期时间

redis 中有一个字典保存了键的过期时间(毫秒时间戳)。

过期键删除策略对比:

定时删除

即在设置过期时间的同时,创建一个timer,一到过期时间就对键进行删除

好处是能快速释放内存,坏处是如果同一时间大量删除操作,会影响CPU性能

惰性删除

即在访问键的时候再删除。对CPU友好但对内存不友好,而且有些键可能之后再也不会访问到

定期删除

在Redis中,实际使用的是配合惰性删除和定期删除策略。所有对键的访问都会先触发惰性删除策略。除此之外,定期删除的任务每次运行时,都按顺序从一定数量的数据库中随机取出一定数量的键进行检查并删除过期键。定期任务到达时间上限时就会停止,等待下一次执行。

对过期键的处理

RDB 功能对过期键的处理

之后会介绍RDB和AOF持久化功能。在生成RDB文件时,会对过期键进行检查,已过期的键不会被保存到RDB文件中。在载入RDB文件时,对于主服务器,会先检查过期键从而不会载入过期键,对于从服务器则不会检查(不过最终会从主服务器同步)

AOF 功能对过期键的处理

只有某个过期键被惰性删除或者定时删除之后,AOF 文件会追加一条 DEL 命令来记录该键已经被删除。加载AOF文件时,也会先检查已过期的键

主从复制模式

主服务器在删除一个过期键之后,会显示的向所有从服务器发送一个 DEL 命令。从服务器只有接收到 DEL 命令后才对键进行删除,不会主动进行惰性删除或者定期删除

数据库通知功能的实现

数据库通知功能可以让客户端订阅给定的频道或者模式,来获知数据库中键的变化。一种是键空间通知(key-space notification),获取某个键执行了什么命令,还有一种是键事件通知(key-event notification),获取的是某个命令被什么键执行了。

实现方式就是在各种命令的实现函数中,比如SADDDEL等命令,都有一个notifyKeyspaceEvent函数,如果命令执行成功则会进入这个函数,函数会将事件名称(命令的名称)、键的名称以及数据库编号拼接成频道的名称,然后进行键空间通知和键事件通知的发送。

RDB 持久化

Redis 是内存数据库,因此需要有持久化功能来保存数据库状态,否则一旦服务器退出数据就会丢失。RDB持久化是通过SAVE或者BGSAVE命令将某个时间点上的数据库状态保存到RDB文件的一个功能,生成的 RDB 文件是一个经过压缩的二进制文件。

SAVE 命令和 BGSAVE 的区别在于前者会阻塞服务器进程直到RDB文件生成完毕,而后者不会阻塞。在执行BGSAVE命令期间,不会接收更多的SAVE或者BGSAVE命令。而对于RDB文件的载入,并没有一个专门的命令,是在服务器启动时,如果检测到有RDB文件,就会自动载入,载入期间也处于阻塞状态。

服务器还可以配置自动保存,需要配置多少时间内进行了多少次修改的保存条件,如果满足条件,服务器就会自动执行BGSAVE命令,默认的配置条件为:

save 900 1 // 即在900s内进行了至少1次修改
save 300 10
save 60 10000

上面设置的保存条件会记录在一个数组中,数组的每个元素有秒数和修改数两个属性。服务器会维护一个dirty计数器以及一个lastsave属性,前者用于记录在上一次成功save或者bgsave之后,进行了修改的次数,后者记录上一次save或者bgsave成功执行的时间,服务器每成功执行一个修改命令就会对这两个值进行更新。

struct redisServer {
    // ...
    long long dirty; // dirty计数器
    time_t lastsave; // 上一次执行保存的时间
    // ...
}

Redis 的轮询函数 serverCron 默认每 100ms 执行一次,其中一项工作就是遍历配置的保存条件,检查是否满足,满足则触发 BGSAVE 命令。

RDB 文件结构

接下来简单介绍,其中常量用全大写表示

  • REDIS:REDIS 五个字符
  • db_version:4 字节字符串整数,版本号
  • database
  • EOF:1 字节
  • check_sum:8 字节校验和

其中 database 部分,如果所有数据库为空,那么 database 也为空;如果有数据库不为空,database 部分会保存多个非空数据库,每个数据库的保存格式如下:

  • SELECTDB:1字节
  • db_number:数据库编号
  • key_value_pairs:保存了该数据库中所有的键值对数据
    • EXPIRETIME_MS:1字节常量。不带过期时间的键值对不保存此值
    • ms:过期时间戳,只有带过期时间的键值对保存此值
    • TYPE
    • key
    • value:不同对象类型和编码的value的格式是不同的,具体格式在《Redis设计与实现》的10.3.3 "value的编码"部分,这里略

AOF 持久化

AOF(Append Only File)持久化功能是通过记录服务器所执行的命令来记录数据库状态的。比如执行了SADDDELSET等修改的命令,这些命令就会一条一条的记录到AOF文件中。服务器启动时,会创建一个不带网络连接的伪客户端来执行AOF文件中的命令,从而通过AOF文件还原数据库的状态。如果服务器开启了AOF持久化,那么会优先加载AOF文件而不是RDB文件。

一条修改命令从成功执行,到最终被写入AOF文件,可以分为以下三个步骤:

  1. 该命令被写入缓冲区
struct redisServer {
    // ...
    sds aof_buf; // AOF缓冲区
}

aof_buf 缓冲区中的内容写入到AOF文件,注意这时实际上只是调用操作系统的函数写入,而通常操作系统并不会直接写入文件,而是会先保存到一个内存缓冲区中,等满足一定条件之后再写入文件。所以这一步相当于内容只写到了操作系统的内存缓冲区中

根据服务器的 appendfsync 配置来决定是否调用操作系统的 fsync/fdatasync 强制将内存缓冲区的内容写入到硬盘。appendfsync有三种配置:

  • always:总是强制同步到硬盘
  • everysec:如果上次同步的时间超过 1s,则再次同步。是默认配置
  • no:不强制同步,等待操作系统同步

AOF重写

随着执行的写命令越来越多,AOF 文件的体积也会越来越大,因此 Redis 提供了 AOF 文件重写的功能,通过一个新的 AOF 文件来代替旧的,两者所保存的数据库状态相同,但新的 AOF 文件不会包含任何冗余命令。

在旧的 AOF 文件中,对同一个键,可能会有很多条写命令,比如对列表先创建再添加再删除再添加等等,这样会产生很多命令,而在新的AOF文件中,只会用一条命令来记录这个列表的最终状态,这样就做到了不记录冗余命令,节省空间。因此,新的AOF文件的生成方式,就是遍历数据库,对每个数据库遍历其中所有的键,根据键的类型对键进行重写,如果带有过期时间,则也会重写。因此新的AOF文件在生成时,只会有SET, SADD, RPUSH, HMSET, ZADD等命令(如果一个键值对的元素过多超过了Redis限制,会拆分成多个命令)。

AOF重写会创建一个子进程,会在后台运行,不会阻塞(BGREWRITEAOF命令);当开始AOF重写后,执行成功的写命令也会写入一个AOF重写缓冲区。重写完成后,AOF重写缓冲区的内容会被写入到新AOF文件中,以保证新AOF文件保存的数据库状态与当前数据库状态一致;之后,对新的AOF文件改名,原子的覆盖现有的AOF文件。重写完成后执行的这两个操作是阻塞的。

这里有个问题,比如有一个list,重写期间,还没有扫描到这个list,客户端进行了一次RPUSH,那么按照上面的解释这个RPUSH会被记录到AOF重写缓冲区,之后扫描到这个list时,会将list最新的值进行记录,最后完成重写时,又多记录了一个RPUSH,这样岂不是会出问题?)(答案就在于创建的子进程,子进程和父进程只会共享创建时候的内存,详细的可以看看这篇文章,强烈推荐:为什么 Redis 快照使用子进程

事件

Redis 服务器是一个事件驱动程序,包括文件事件比如客户端的请求,以及时间事件比如Redis服务器中的周期任务。

对于文件事件,采用IO多路复用来同时监听多个套接字。相应的操作有连接应答(accept)、读取(read)、写入(write)、关闭(close),这些操作都会产生一个文件事件,文件事件分派器会根据事件类型调用相应的事件处理器:

  • 连接应答:对连接服务器的各个客户端进行应答;连接应答处理器
  • 读取:接收客户端的命令请求;命令请求处理器
  • 写入:向客户端返回命令的执行结果; 命令回复处理器
  • 主从复制:复制处理器

对于时间事件,主要有自增ID、执行时间戳、处理函数(时间事件处理器)几个属性;所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,就遍历整个链表找到所有已到达的时间事件(已到达是指执行时间戳小于等于当前时间),并调用相应的处理函数(因为目前基本上只有serverCron一个时间事件,所以遍历不会影响性能);处理完之后,如果这是一个周期任务(根据处理函数的返回值判断),还会更新该事件的执行时间戳属性。

serverCron 函数的主要工作有:

  • 更新服务器的各类统计信息
  • 清理过期键值对
  • 关闭连接失效的客户端
  • 尝试进行AOF或RDB操作
  • 如果是主服务器,对从服务器定期同步

文件事件和时间事件是同步执行的,不会进行抢占。服务器会计算离最近的时间事件开始的时间间隔,在这个期间监听文件事件,如果有文件事件,执行完文件事件之后会执行所有已到达的时间事件(因此时间事件的处理时间通常会比设定的要晚一些),如果没有已到达的时间事件,则重复上面的步骤。

客户端

服务器中用 redisClient 结构体记录了客户端的状态,所有 redisClient 用一个链表记录,新创建的 redisClient 添加到链表的末尾。下面介绍一下 redisClient 中的常见属性

typedef struct redisClient {
    // ...
    int fd; // 套接字描述符,伪客户端为-1比如加载AOF文件时的伪客户端,普通客户端为大于-1的整数

    robj *name; // 客户端名字,默认没有设置名字

    int flags; // 一些标志,比如是否为主/从客户端

    sds querybuf; // 输入缓冲区,用于保存客户端发送的命令请求。缓冲区大小是动态的,但不能超过1G否则客户端将被关闭
    robj **argv; // 解析得到的命令参数,是一个数组,每个元素都是字符串,比如"set" "key" "value"
    int argc; // argv数组的长度
    struct redisCommand *cmd; // 根据argv[0]的值对应的命令实现函数,比如set

    // 输出缓冲区
    char buf[REDIS_REPLY_CHUNK_BYTES]; // 输出缓冲区,固定大小,默认为16k
    int bufpos; // 记录了buf已使用的字节数
    list *reply; // 输出缓冲区,可变大小,当回复无法放进buf时,会放入该链表中,链表中元素是字符串对象

    // 时间相关
    time_t ctime; // 客户端的创建时间
    time_t lastinteraction; // 最后一次交互时间,可以是客户端向服务端也可以是相反
    time_t obuf_soft_limit_reached_time; // 输出缓冲区第一次达到软性限制的时间。
    // 如果输出缓冲区的大小超过了硬性限制那么客户端将会被立即关闭。
    // 如果超过了软性限制而没超过硬性限制,则该属性会记录到达软性限制的时间,
    // 如果在指定时间内不再超出软性限制,则客户端不会被关闭,并且该属性重置为0,否则客户端会被关闭
} redisClient;

服务器

首先介绍一下服务器如何执行客户端的命令。

  1. 在解析得到客户端的 argv 属性后,命令执行器首先会根据 argv[0] 参数,在命令表中查找参数对应的命令,命令表是一个字典,键是命令的名字,值是一个个 redisCommand 结构,该结构中有一个 proc 属性指向命令的实现函数;
  2. 命令执行器会进行一些预备操作,包括:
    • 检查客户端的cmd指针是否指向NULL,是的话直接返回错误
    • 检查命令的参数个数是否正确,不正确则直接返回错误
    • 检查客户端是否已经通过了身份验证,未通过验证则只能执行AUTH命令
    • 很多其他的一些预备操作,略过
  3. 调用命令的实现函数,将回复保存到客户端的输出缓冲区
  4. 执行后续工作,比如
    • 检查是否需要添加慢日志
    • 更新redisCommand结构中的milliseconds属性(命令执行时长)和calls属性(命令执行次数)
    • 是否进行AOF持久化
    • 是否将命令发送给从服务器

接下来介绍一下serverCron函数,默认每100ms运行一次,有以下工作,大部分都涉及到redisServer结构中的属性:

  1. 更新服务器时间缓存,在一些精度要求不高的场景减少系统调用,比如打印日志、计算uptime、计算键的idle time等,对于过期时间等场景还是需要获取准确的时间
  2. 更新服务器每秒执行命令次数。根据抽样进行估算,略
  3. 更新服务器内存峰值记录
  4. 处理SIGTERM信号。当服务器接收到SIGTERM信号时,redisServer中的shutdown_asap会被置为1,serverCron如果检测到该值为1,就会进行RDB持久化,然后关闭服务器
  5. 管理客户端资源。比如连接超时释放客户端,释放客户端输入缓冲区
  6. 关闭输出缓冲区大小超出限制的客户端
  7. 管理数据库资源。比如删除过期键
  8. 执行被延迟的BGREWRITEAOF。如果在执行BGSAVE期间有BGREWRITEAOF命令,那么redisServer中的aof_rewrite_scheduled会被置为1,serverCron会检查是否可以执行BGREWRITEAOF
  9. 将AOF缓冲区中的内容写入AOF文件
  10. 如果有BGREWRITEAOF或者BGSAVE命令在运行(通过redisServer中记录两者的pid的属性查看),检查是否有信号到达,如果有则表示AOF已经重写完毕或者新的RDB文件已经生成完毕,则执行后续操作,进行新旧文件的替换。如果没有这两个命令在运行,则检查是否有BGREWRITEAOF被延迟了,如果没有,检查是否满足自动保存的条件(BGSAVE),如果不满足,检查是否满足AOF重写的条件。如图:

最后介绍一下服务器的初始化:

  1. initServerConfig,初始化一些基本属性,载入配置
  2. initServer,初始化一些数据结构,创建共享对象,设置进程信号处理器,打开监听端口,为 serverCron 函数创建时间事件,等等
  3. 还原数据库状态,AOF 或者 RDB 文件载入
  4. 执行事件循环

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84960 人气
更多

推荐作者

lorenzathorton8

文章 0 评论 0

Zero

文章 0 评论 0

萧瑟寒风

文章 0 评论 0

mylayout

文章 0 评论 0

tkewei

文章 0 评论 0

17818769742

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文