Redis(五). 运行机制(AOF RDB)

Redis(五). 运行机制

1.数据库

1.1 数据库结构

每一个数据库的结构 如下

/* Redis database representation. There are multiple databases identified
 * by integers from 0 (the default database) up to the max configured
 * database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB */   保存着数据库中的所有键值对数据 键空间(key space)
    dict *expires;              /* Timeout of keys with a timeout set */   保存着键的过期信息
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/ 实现列表阻塞原语,如 BLPOP
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */  用于实现 WATCH 命令
    int id;                     /* Database ID */  保存着数据库以整数表示的号码
    long long avg_ttl;          /* Average TTL, just for stats */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
    clusterSlotToKeyMapping *slots_to_keys; /* Array of slots to keys. Only used in cluster mode (db 0). */
} redisDb;

1.2 切换数据库 id

id 域来了解自己正在使用的是哪个数据库,不用指针来一个个判断

1.3 数据库键空间

因为 Redis 是一个键值对数据库(key-value pairs database),所以它的数据库本身也是一个字典(俗称 key space)

1.4 键的过期时间

127.0.0.1:6379> setex mykey 10 1111  //设置过期时间
OK
127.0.0.1:6379> ttl mykey
(integer) 5
127.0.0.1:6379> get mykey
(nil)

1.5 过期时间的保存

redisDb 结构的 expires 字典里

typedef struct redisDb {
// ...
dict *expires;  //用字典保存   value 存储的是到期时间
// ...
} redisDb;

在这里插入图片描述

1.6 设置过期时间

Redis 有四个命令可以设置键的生存时间(可以存活多久)和过期时间(什么时候到期):
EXPIRE 以秒为单位设置键的生存时间;
PEXPIRE 以毫秒为单位设置键的生存时间;
EXPIREAT 以秒为单位,设置键的过期 UNIX 时间戳;
PEXPIREAT 以毫秒为单位,设置键的过期 UNIX 时间戳。

虽然有那么多种不同单位和不同形式的设置方式,但是 expires 字典的值只保存“以毫秒为单位的过期 UNIX 时间戳

1.7 过期键的判断

通过 expires 字典,可以用以下步骤检查某个键是否过期:

  1. 检查键是否存在于 expires 字典:如果存在,那么取出键的过期时间;
  2. 检查当前 UNIX 时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则,键未过期。

1.8 过期键的删除

1.定时删除,定时事件,到达过期时间时,事件执行删除;内存友好,CPU不友好,效率较低
2.惰性删除:每次取出时,判断时间,若过期,则删除;内存不友好,过期键不被访问长期占用内存
3.定期删除:定时任务一样对expires 字典检测,删除过期;折中策略,每隔一段时间执行删除任务,时间可控,减少CPU时间占用

Redis 使用惰性删除 + 定期删除 相互配合

1.9 过期键的惰性删除策略

写请求 ->判断是不是过期,过期删除 —> 在执行命令

读请求-> 判断是不是过期,过期删除 --> 过期是nil /返回value

1.10 过期键的定期删除策略

定期删除由 redis.c/activeExpireCycle 函数执行,函数在规定的时间限制内,尽可能地遍历各个数据库的 expires 字典,随机地检查一部分键的过期时间,并删除其中的过期键;

1.11 过期键对 AOFRDB 和复制的影响

RDB文件

在创建新的 RDB 文件时,程序会对键进行检查,过期的键不会被写入到更新后的 RDB 文件中。因此,过期键对更新后的 RDB 文件没有影响。

AOF文件

先不做处理。当过期键被惰性删除、或者定期删除之后,程序会向 AOF 文件追加一条 DEL 命令,来显式地记录该键已被删除

AOF重写

和 RDB 文件类似,当进行 AOF 重写时,程序会对键进行检查,过期的键不会被保存到重写后的 AOF 文件。因此,过期键对重写后的 AOF 文件没有影响。

集群复制

过期键的删除由主节点统一控制

  • 如果服务器是主节点,那么它在删除一个过期键之后,会显式地向所有附属节点发送一个 DEL 命令
  • 服务器是附属节点,请求主节点删除,当接到从主节点发来的 DEL 命令之后,附属节点才会真正的将过期键删除掉

1.12 数据库收缩扩容

规则个前面说的数据结构字典完全一样

2.RDB

Redis 以数据结构的形式将数据维持在内存中,为了让这些数据在 Redis 重启之后仍然可用,Redis 分别提供了 RDB 和 AOF 两种持久化模式

在 Redis 运行时,RDB 程序将当前内存中的数据库快照保存到磁盘文件中,在 Redis 重启动时,RDB 程序可以通过载入 RDB 文件来还原数据库的状态

在这里插入图片描述

2.1 保存

SAVEBGSAVE 两个命令都会调用 rdbSave 函数,但它们调用的方式各有不同:

SAVE 直接调用 rdbSave ,阻塞 Redis 主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。SAVE 执行时新的SAVEBGSAVEBGREWRITEAOF 调用都不会产生任何作用,服务器会检查 BGSAVE 是否正在执行当中,如果是的话,服务器就不调用 rdbSave ,而是向客户端返回一个出错信息,告知在 BGSAVE 执行期间,不能执行SAVE

BGSAVE 则 fork 出一个子进程,子进程负责调用 rdbSave ,并在保存完成之后向主进程发送信号,通知保存已完成。因为 rdbSave 在子进程被调用,所以 Redis 服务器在BGSAVE 执行期间仍然可以继续处理客户端的请求。当 BGSAVE 正在执行时,调用新 BGSAVE 命令的客户端会收到一个出错信息,告知 BGSAVE 已经在执行当中。

2.2载入

Redis 服务器启动时,rdbLoad 函数就会被执行,它读取 RDB 文件,并将文件中的数据库数据载入到内存中;

等到载入完成之后,服务器才会开始正常处理所有命令。只有 PUBLISH 、SUBSCRIBE 、PSUBSCRIBE 、UNSUBSCRIBE 、PUNSUBSCRIBE 五个命令的请求会被正确地处理,其他的错误

:发布与订阅功能和其他数据库功能是完全隔离的,前者不写入也不读取数据库,所以在服务器载入期间,订阅与发布功能仍然可以正常使用,而不必担心对载入数据的完整性产生影响

AOF 文件的保存频率通常要高于 RDB 文件保存的频率,那么程序优先使用 AOF 文件来还原数据。只有在 AOF 功能未打开的情况下,Redis 才会使用 RDB 文件来还原数据;

2.3 RDB 文件结构

在这里插入图片描述

内容长度或大小含义
REDIS5个字符标识着一个 RDB 文件的开始
RDB-VERSION文件版本RDB 版本号
DB-DATA保存的数据
EOF标志着数据库内容的结尾(不是文件的结尾) ,值为 255
CHECK-SUMuint_64t 类型值RDB 文件所有内容的校验和

3.AOF

AOF 则以协议文本的方式,将所有对数据库进行过写入的命令(及其参数)记录到 AOF文件,以此达到记录数据库状态的目的;(逻辑备份类似)

在这里插入图片描述

3.1 AOF 命令同步

在客户端执行命令

127.0.0.1:6379[2]> lpush mylist 11 22 33
(integer) 3
127.0.0.1:6379[2]> keys *
1) "mylist"
127.0.0.1:6379[2]> lpop mylist
"33"
127.0.0.1:6379[2]> lpush mylist 33333
(integer) 3
127.0.0.1:6379[2]>

对数据库有修改的写入命令就会被同步到 AOF 文件中:

lpush mylist 11 22 33
lpop mylist
lpush mylist 33333

的四个命令在 AOF 文件中就实际保存如下:

*6  //本条指令/参数 6个 字符串 组成
$5 //一条指令5 个字符
RPUSH    //第1个
$6 //一条指令6 个字符
mylist      //第2个
$1  // 一条指令1个字符
1        //第3个
$1  // 一条指令1个字符
2       //第4个
$1  // 一条指令1个字符
3       //第5个   
$1
4       //第6个

*2//本条指令 2个 字符串 组成
$4 //一条指令4 个字符
LPOP    //第1个
$6 //一条指令6 个字符
mylist    //第2个

*3 //本条指令 3个 字符串 组成
$4  //一条指令4 个字符
LPUSH
$4  //一条指令6 个字符
mylist 
$5
33333

AOF 文件写入三个步骤

  • 命令传播:Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。
  • 缓存追加:AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的 AOF 缓存中。
  • 文件写入和保存:AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保存条件被满足的话,fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。

3.2 命令传播

当一个 Redis 客户端需要执行命令时,它通过网络连接,将协议文本发送给 Redis 服务器

SET KEY VALUE  
"*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n"

3.3 缓存追加

当命令被传播到 AOF 程序之后,程序会根据命令以及命令的参数,将命令从字符串对象转换回原来的协议文本;它会被追加到 redis.h/redisServer 结构的 aof_buf 末尾

3.4 文件写入和保存

执行以下两个工作:

  • WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。

  • SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。

3.5 AOF 保存模式

  1. AOF_FSYNC_NO :不保存。
  2. AOF_FSYNC_EVERYSEC :每一秒钟保存一次。
  3. AOF_FSYNC_ALWAYS :每执行一个命令保存一次。

在这里插入图片描述

3.6 AOF 文件的读取和数据还原

Redis 读取 AOF 文件并还原数据库的详细步骤如下:

  1. 创建一个不带网络连接的伪客户端(fake client)。
  2. 读取 AOF 所保存的文本,并根据内容还原出命令、命令的参数以及命令的个数。
  3. 根据命令、命令的参数和命令的个数,使用伪客户端执行该命令。
  4. 执行 2 和 3 ,直到 AOF 文件中的所有命令执行完毕。

3.7 AOF 重写

AOF 文件通过同步 Redis 服务器所执行的命令,从而实现了数据库状态的记录,但是,这种同步方式会造成一个问题:随着运行时间的流逝,AOF 文件会变得越来越大;

RPUSH list 1 2 3 4 // [1, 2, 3, 4]
RPOP list // [1, 2, 3]
LPOP list // [2, 3]
LPUSH list 1 // [1, 2, 3]

被频繁操作的键,对它们所调用的命令可能有成百上千、甚至上万条;AOF 文件的体积就会急速膨胀;

如果我们要保存这个列表的当前状态,并且尽量减少所使用的命令数,那么最简单的方式不是去 AOF 文件上分析前面执行的四条命令,而是直接读取 list 键在数据库的当前值,然后用一条 RPUSH 1 2 3 命令来代替前面的四条命令;

3.8 AOF 后台重写

Redis 不希望 AOF 重写造成服务器无法处理请求,所以Redis 决定将 AOF 重写程序放到(后台)子进程里执行,这样处理的最大好处是

  • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求
  • 子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性

问题:新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的AOF 文件中的数据不一致

解决:Redis 主进程在接到新的写命令之后,除了会将这个写命令的协议内容追加到现有的 AOF文件之外,还会追加到这个缓存中;

在这里插入图片描述

3.9 AOF重写触发条件

  • 调用 BGREWRITEAOF 手动触发。

查以下条件是否全部满足,如果是的话,就会触发自动的 AOF 重写:

  1. 没有 BGSAVE 命令在进行。
  2. 没有 BGREWRITEAOF 在进行。
  3. 当前 AOF 文件大小大于 server.aof_rewrite_min_size (默认值为 1 MB)。
  4. 当前 AOF 文件大小和最后一次 AOF 重写后的大小之间的比率大于等于指定的增长百分比。(默认增长百分比为 100%)

4. 事件

处理文件事件:在多个客户端中实现多路复用,接受它们发来的命令请求,并将命令的执行结果返回给客户端。

时间事件:实现服务器常规操作(server cron job)

4.1 文件事件

多个客户端通过套接字连接到 Redis 服务器中,但只有在套接字可以无阻塞地进行读或者写时,服务器才会和这些客户端进行交互。

Redis 将这类因为对套接字进行多路复用而产生的事件称为文件事件(file event),文件事件可以分为读事件和写事件两类

读事件

读事件标志着客户端命令请求的发送状态。

  • 当客户端只是连接到服务器,但并没有向服务器发送命令时,该客户端的读事件就处于等待状态。

  • 当客户端给服务器发送命令请求,并且请求已到达时(相应的套接字可以无阻塞地执行读操作),该客户端的读事件处于就绪状态。

在这里插入图片描述

客户端 X 向服务器发送命令请求,并且命令请求已到达时,客户端 X 的读事件状态变为就绪

客户端 X就绪已发送,并且已到达
客户端读事件状态命令发送状态
客户端 Y等待未发送
客户端 Z等待未发送

写事件

写事件标志着客户端对命令结果的接收状态

一个写事件会在两种状态之间切换:

  • 当服务器有命令结果需要返回给客户端,但客户端还未能执行无阻塞写,那么写事件处于等待状态。

  • 当服务器有命令结果需要返回给客户端,并且客户端可以进行无阻塞写,那么写事件处于就绪状态。

示例:服务器正等待客户端 X 变得可写,从而将命令的执行结果返回给它

客户端读事件状态写事件状态
客户端X等待已就绪
客户端 Y等待
客户端 Z等待

当命令执行结果被传送回客户端之后,客户端和写事件之间的关联会被解除(只剩下读事件)

4.2 时间事件

时间事件记录着那些要在指定时间点运行的事件,多个时间事件以无序链表的形式保存在服务器状态中

  • when :以毫秒格式的 UNIX 时间戳为单位,记录了应该在什么时间点执行事件处理函数
  • timeProc :事件处理函数 (根据返回值判读是否是一次事件,处理一次的事件执行完成之后就会被删除不在执行,否则的话更新when 下次继续执行)
  • next : 下一个时间事件

4.3 时间事件应用实例:服务器常规操作

redis.c/serverCron 执行内容;每隔 10 毫秒就会被运行一次;而具体的间隔可以由用户自己调整

  • 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等
  • 清理数据库中的过期键值对。
  • 对不合理的数据库进行大小调整。
  • 关闭和清理连接失效的客户端。
  • 尝试进行 AOF 或 RDB 持久化操作。
  • 如果服务器是主节点的话,对附属节点进行定期同步。
  • 如果处于集群模式的话,对集群进行定期同步和连接测试;

4.4 事件的执行与调度

Redis 里面的两种事件呈合作关系,它们之间包含以下三种属性:

  • 一种事件会等待另一种事件执行完毕之后,才开始执行,事件之间不会出现抢占
  • 事件处理器先处理文件事件(处理命令请求),再执行时间事件(调用 serverCron)
  • 文件事件的等待时间(类 poll 函数的最大阻塞时间),由距离到达时间最短的时间事件决定;

文件事件的优先级高(客户端的连接一般),事件是串行的,所以会等待文件事件结束才会执行,所以调度的时候会等待文件事件执行完成之后在执行,所以有时候比约定的时间要晚一点;

5.服务器和客户端

启动 Redis 服务器,到服务器可以接受外来客户端的网络连接这段时间,Redis 需要执行一系列初始化操作;

  • 初始化服务器全局状态。
  • 载入配置文件。
  • 创建 daemon 进程。
  • 初始化服务器功能模块。
  • 载入数据。
  • 开始事件循环。

5.1 初始化服务器全局状态

服务器中的所有数据库;命令表;事件状态;服务器的网络连接信息:套接字地址、端口,以及套接字描述符;所有已连接客户端的信息;Lua 脚本的运行环境;实现订阅与发布(pub/sub)功能所需的数据结构;日志(log)和慢查询日志(slowlog);数据持久化(AOF 和 RDB)的配置和状态;服务器配置选项;统计信息;程序创建一个 redisServer 结构的实例变量 server 用作服务器的全局状态,并将server 的各个属性初始化为默认值。

5.2载入配置文件

程序为 server 变量(也即是服务器状态)的各个属性设置了默认值,但这些默认值有时候并不是最合适的;

  • 用户可能想使用 AOF 持久化,而不是默认的 RDB 持久化。
  • 用户可能想用其他端口来运行 Redis ,以避免端口冲突。
  • 用户可能不想使用默认的 16 个数据库,而是分配更多或更少数量的数据库。
  • 用户可能想对默认的内存限制措施和回收策略做调整

5.3创建 daemon 进程

Redis 默认以 daemon 进程的方式运行;当服务器初始化进行到这一步时,程序将创建 daemon 进程来运行 Redis ,并创建相应的 pid文件

5.4初始化服务器功能模块

为 server 变量的数据结构子属性分配内存 ;初始化这些数据结构

初始化 Redis 进程的信号功能;初始化日志功能;初始化客户端功能;初始化共享对象;初始化数据库;初始化网络连接;初始化各个统计变量;完成这一步之后,服务器打印出 Redis 的 ASCII LOGO 、服务器版本等信息,表示所有功能模块已经就绪,可以等待被使用了

5.5载入数据

程序需要将持久化在 RDB 或者 AOF 文件里的数据,载入到服务器进程里面;服务器打印出一段载入完成信息:

[6717] 22 Feb 11:59:14.830 * DB loaded from disk: 0.068 seconds

5.6开始事件循环

[6717] 22 Feb 11:59:14.830 * The server is now ready to accept connections on port 6379

以下是初始化完成之后,服务器状态和各个模块之间的关系图

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值