数据库和事件

这是《Redis设计与实现》系列的文章,系列导航:Redis设计与实现笔记

数据库

数据库的结构定义在 redis.h/redisServer 这个结构体中,这个结构体有许多的字段用以记录 Redis 数据库的状态。学习数据库的过程中会慢慢地接触这里面的各种字段。

  1. struct redisServer {
  2. //...
  3. }

不同的数据库

在上述结构中, int dbnum 保存了数据库的数量,默认为16;redisDb *db 指向了服务器中所有的数据库(如下 1 处)

而客户端 redisClient 中也有一个 redisDB *db 指向当前选用的数据库(如 2 处)。

我们可以用 SELECT N 来选择第 N 个数据库,但是一般并不推荐用多个数据库,因为 Redis 没有命令可以获取当前用的是哪个数据库,这样会容易造成使用错数据库的尴尬场景。

数据库中的数据

Redis 是一个键值对的数据库服务器,服务器的每一个数据库都由一个 redis.h/redisDB 结构表示,该结构中的 dict 字典保存了数据库中的所有键值对,我们称这个字典为键空间

  1. typedef struct redisDb {
  2. //...
  3. dict *dict;
  4. //...
  5. }redisDb;

键空间保存了所有对象,键一般为字符串名称,而值则对应各种类型的对象,如:

针对这些键值对,可以进行各种增删改查的操作。

另外,其他的一些命令如EXISTSRENAMEKEYSFLUSHDBRANDOMKEYDBSIZE 都是通过对键空间进行操作来实现的。

读写键空间时的维护操作:

  1. 更新服务器中键空间命中和不命中的次数
  2. 更新键的LRU时间
  3. 如果服务器在读取一个键时发现已经过期,则会先删除这个键,再执行余下的操作
  4. 如果有客户端使用WATCH监视了某个键,那么服务器在对被监视的键进行修改后,会将这个键标记为脏,从而让事务程序注意到这个键已经被修改了
  5. 服务器修改一个键,都会对脏键计数器的值 + 1,这个计数器会触发服务器的持久化以及复制操作
  6. 如果开启了通知,会按照配置发送相应的数据库通知

超时管理

可以通过EXPIREPEXPIREEXPIREATPEXPIREAT来设置过期的剩余时间或时间。

这几种的命令都是通过 PEXPIREAT 来实现的。

保存过期时间

redisDb 结构的 expires 字典保存了数据库中所有键的过期时间,称之为过期字典

过期字典的键为一个指针,指向键空间某个键对象

过期字典的值为 long long 类型的整数,保存了所有键所指向的数据库键的过期时间

一个结构图:

给键的过期时间进行增删改查就相当于对这个字典进行增删改查,很容易想到如何做,就不多赘述了。

过期删除策略

三种常见过期删除策略:

  • 定时删除:在设置过期时间的同时设置定时器,让定时器执行删除
  • 惰性删除:查询时才判断是否过期及删除
  • 定期删除:每隔一段时间对数据库进行检查,删除其中的过期键

对比:

策略 优点 缺点
定时删除 对内存友好 对CPU不友好
惰性删除 对CPU友好 对内存不友好,不访问就不释放了
定期删除 是前两种的折中 需要合理配置参数

Redis 服务器实际使用的是惰性删除定期删除两种策略。

其中惰性删除db.c/expireIfNeeded 函数实现,所有读写数据库的 Redis 命令在执行之前都会调用其对输入键进行检查。

定期删除redis.c/activeExpireCycle 函数实现,每当 Redis 的服务器周期性操作 redis/serverCron 函数执行时,这个函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的 expires 字典中随机检查一部分键的过期时间,并删除其中的过期键。

对备份的影响

RDB:

  • 创建时,会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中。
  • 导入时,如果是以主服务器模式运行,则忽略过期键;

    如果是以从服务器模式运行,则不论是否过期全部导入。

AOF:

  • 如果数据库中某个键过期了但没有被删除,则什么都不做
  • 如果过期了且被删除了,就在AOF文件中追加一条 DEL 命令
  • AOF重写时,会过滤掉过期的键

对主从复制的影响

复制模式下,从服务器的删除由主服务器控制:

  • 主服务器过期,则分发 DEL 命令
  • 从服务器过期,不会将过期键删除,而是像没过期一样处理过期键

由主服务器控制删除,可以保证主从数据一致性。

通知功能

通知功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化。

通知分为:

  • 键空间通知:某个键执行了什么命令
  • 键事件通知:某个命令什么时候被执行

通过配置 notify-keyspace-events 来决定服务器所发送通知的类型:

  • AKE:空间 + 事件
  • AK:空间
  • AE:事件
  • K$:只发送和字符串键相关的键空间通知
  • El:只发送和列表键相关的键事件通知

(这里只是列举部分,具体用法请参阅文档)

发送通知

  1. void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid);

type 是类型,程序根据这个值和前面的配置进行对比以确定是否发送通知。

剩下的三个分别是事件的名称产生事件的键产生事件的数据库号码,函数会根据这三个参数来构建通知的内容接收通知的频道名称

例如 SADD 命令的实现函数中发送通知的相关代码:

  1. void saddCommand(redisClient *c) {
  2. // ...
  3. if (added) {
  4. notifyKeyspaceEvent(REDIS_NOTIFY_SET, "sadd", c->argv[1], c->db-id);
  5. }
  6. // ...
  7. }

发送通知的实现

当调用 pubsubPublishMessage 发送通知后,订阅数据库通知的客户端就会收到这个消息。

事件

Redis是一个事件驱动程序,服务器需要处理以下两类事件:

  • 文件事件:主要是和套接字(也就是网络)有关的事件
  • 时间事件serverCron 等需要在特定的时间执行

文件事件

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

  • 使用 I/O 多路复用程序 来同时监听多个套接字
  • 当被监听的套接字准备好执行连接应答、读取、写入、关闭等操作时,与操作相关的文件事件就会产生,文件事件处理器就会调用关联的事件处理器进行处理

虽然文件事件处理器以单线程方式运行,但通过使用 I/O多路复用程序 来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接,这保持了Redis内部单线程设计的简单性。

上述内容牵扯到了网络编程的相关知识,等我学完相关部分再来补充!

构成

四个部分:

  1. 套接字
  2. I/O多路复用程序
  3. 文件事件分派器
  4. 事件处理器

尽管多个文件事件可能并发发生,但是IO多路复用程序会把他们全放入一个队列里,让文件时间分派器依次处理。

实现

通过包装常见的selectepollevportkqueue 这些I/O多路复用函数库来实现。

因为 Redis 为每个 I/O多路复用函数库都实现了相同的 API,所以这些程序的底层实现是可以互换的。

程序会在编译时自动选择系统中性能最高的 I/O多路复用函数库来作为 Redis 的 I/O多路复用程序的底层实现。

事件类型

  • AE_READABLE:当套接字变得可读,如客户端执行 write、close操作,或者有了新的可应答套接字时产生
  • AE_WRITABLE:当套接字变得可写,如客户端执行了 read 操作时产生

如果一个套接字即可读又可写,那么先读后写。

文件事件处理器

  • 连接应答处理器:对连接服务器的各个客户端进行应答
  • 命令请求处理器:接收客户端传来的命令请求
  • 命令回复处理器:向客户端返回命令的执行结果
  • 复制处理器:主从服务器进行复制时为复制功能关联

时间事件

时间事件分为:

  1. 定时事件
  2. 周期性事件

一个时间事件的组成:

  • id

  • when:UNIX时间戳,记录事件到达时间

  • timeProc:时间事件处理器,一个函数,时间事件到达时执行

    根据这个函数的返回值是 ae.h/AE_NOMORE 或整数值来判断是周期事件还是定时事件。

实现

无序链表保存所有事件,每当时间事件执行器运行时,他就遍历整个链表(因为无序),查找可以执行的事件。

serverCron函数

这个函数在前面和之后都会反复提到,它负责对 Redis 自身的资源和状态进行检查和调整,具体职责包括:

  • 更新服务器的各种统计信息,比如时间、内存占用、数据库占用情况等
  • 清理数据中的过期键值对
  • 关闭和清理连接失效的客户端
  • 尝试进行持久化操作
  • 如果是主服务器,则对从服务器进行定期的同步
  • 如果处于集群模式,对集群进行定期同步和连接测试

事件的调度和执行

Redis 主函数的逻辑和伪代码:

说明如下(摘自原文):

  1. aeApiPoll 函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这个方法既可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保aeApiPoll函数不会阻塞过长时间。
  2. 因为文件事件是随机出现的,如果等待并处理完一次文件事件之后,仍未有任何时间事件到达,那么服务器将再次等待并处理文件事件。随着文件事件的不断执行,时间会逐渐向时间事件所设置的到达时间逼近,并最终来到到达时间,这时服务器就可以开始处理到达的时间事件了。
  3. 对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占,因此,不管是文件事件的处理器,还是时间事件的处理器,它们都会尽可地减少程序的阻塞时间,并在有需要时主动让出执行权,从而降低造成事件饥饿的可能性。比如说,在命令回复处理器将一个命令回复写人到客户端套接字时,如果写入字节数超过了一个预设常量的话,命令回复处理器就会主动用break跳出写人循环,将余下的数据留到下次再写;另外,时间事件也会将非常耗时的持久化操作放到子线程或者子进程执行。
  4. 因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理事件通常会晚一点。

(由于这一段高度概括,之后看源码再仔细学习)

Redis设计与实现2.1:数据库和事件的更多相关文章

  1. 探索Redis设计与实现9:数据库redisDb与键过期删除策略

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  2. Redis设计与实现——多机数据库的实现

    复制 旧版Redis的复制功能分为同步(sync)和命令传播两个操作. sync:是一个非常耗费资源的操作                                           命令传播 ...

  3. 【笔记】《Redis设计与实现》chapter9 数据库

    9.1 服务器中的数据库 Redis服务器将所有都保存在服务器状态redis.h/redisServer结构中 struct redisServer{ //... // 一个数组,保存着服务器中所有数 ...

  4. Redis | 第4章 Redis中的数据库《Redis设计与实现》

    目录 前言 1. Redis中的数据库 2. 数据库的键空间 3. 键的生成时间与过期时间 4. Redis中的过期键删除策略 5. AOF.RDB和复制功能对过期键的处理 5.1 生成 RDB 文件 ...

  5. 《Redis设计与实现》- 数据库

    1. 服务器中数据库结构 Redis 服务器将所有数据库都保存在服务器状态 redisServer 结构的 db 数组中,由 redisDb 结构代表一个数据库 struct redisServer ...

  6. Redis设计与实现——单机数据库的实现

    数据库 服务器中的数据库 redisClient切换数据库 redis客户端默认目标数据库为0号数据库,可以通过SELECT命令来切换目标数据库. 客户端状态redisClient结构的db属性记录了 ...

  7. Redis 设计与实现:数据库

    本文的分析都是基于 Redis 6.0 版本源码 redis 6.0 源码:https://github.com/redis/redis/tree/6.0 服务器中的数据库 Redis 服务器将绝大部 ...

  8. Redis设计与实现(一~五整合版)【搬运】

    Redis设计与实现(一~五整合版) by @飘过的小牛 一 前言 项目中用到了redis,但用到的都是最最基本的功能,比如简单的slave机制,数据结构只使用了字符串.但是一直听说redis是一个很 ...

  9. 《Redis设计与实现》读书笔记

    <Redis设计与实现>读书笔记 很喜欢这本书的创作过程,以开源的方式,托管到Git上进行创作: 作者通读了Redis源码,并分享了详细的带注释的源码,让学习Redis的朋友轻松不少: 阅 ...

随机推荐

  1. html5网页录音和语音识别

    背景 在输入方式上,人们总是在追寻一种更高效,门槛更低的方式,来降低用户使用产品的学习成本.语音输入也是一种尝试较多的方式,有些直接使用语音(如微信语音聊天),有些需要将语音转化为文字(语音识别).接 ...

  2. 结合Vue.js的前端压缩图片方案

    这是一个很简单的方案.嗯,是真的. 为什么要这么做? 在移动Web蓬勃发展的今天,有太多太多的应用需要让用户在移动Web上传图片文件了,正因如此,我们有些困难必须去攻克: 低网速下上传进度缓慢,用户体 ...

  3. Vue小说阅读器(仿追书神器)

    一个vue阅读器项目,目前已升级到2.0,阅读器支持横向分页并滑动翻页(没有动画,需要动画的可以自己设置,增加transitionDuration即可) 技术栈 vue全家桶+mint-ui gith ...

  4. CSS的inline、block与inline-block

    基本知识点 行内元素一般是内容的容器,而块级元素一般是其他容器的容器,行内元素适合显示具体内容,而块级元素适合做布局. 块级元素(block):独占一行,对宽高的属性值生效:如果不给宽度,块级元素就默 ...

  5. Android实现蓝牙远程连接遇到的问题

    主要问到的问题:1.uuid获取不到,一直为空,后来发现android4.2之前使用uuid这种方法,目前尽量不使用uuid方式 2.socket.connect()出错,报read failed, ...

  6. EMS设置发送连接器和接收连接器邮件大小

    任务:通过EMS命令设置发送接收连接器和接收连接器的邮件大小限制值为50MB. 以Exchange管理员身份打开EMS控制台.在PowerShell命令提示符下. 键入以下命令设置接收-连接器的最大邮 ...

  7. Jenkins+gitlab手动部署

    环境: Jenkins:172.16.88.221 (安装Jenkins和git命令) gitlab:172.16.88.221 (安装gitlab) 远程部署机器:172.16.88.220 (安装 ...

  8. 邮件任务-springboot

    邮件任务-springboot springboot可以很容易实现邮件的发送 具体实现步骤: 导入jar包 <dependency> <groupId>org.springfr ...

  9. 虚拟机安装linux

    https://blog.csdn.net/wujiele/article/details/92803655https://www.cnblogs.com/yunwangjun-python-520/ ...

  10. Mysql入门学习day2随笔2

    事务 什么是事务 要么都成功,要么都失败 事务原则 原子性:针对一个事务,两个步骤一起成功或一起失败 一致性:最终一致性,例如A.B之间的转账,无论两个账户如何操作,两账户的总价值不会变 隔离性:针对 ...