Redis底层原理--05. Redis 数据库

1数据库

1.1 数据结构

Redis 中的每个数据库,都由一个 redis.h/redisDb 结构表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct redisDb {
// 保存着数据库以整数表示的号码
int id;
// 保存着数据库中的所有键值对数据
// 这个属性也被称为键空间(key space)
dict *dict;
// 保存着键的过期信息
dict *expires;
// 实现列表阻塞原语,如 BLPOP
// 在列表类型一章有详细的讨论
dict *blocking_keys;
dict *ready_keys;
// 用于实现 WATCH 命令
// 在事务章节有详细的讨论
dict *watched_keys;
} redisDb;

具体事例:

Redis 数据库

1.2 设置生存时间

Redis 有四个命令可以设置键的生存时间(可以存活多久)和过期时间(什么时候到期):

  • EXPIRE 以秒为单位设置键的生存时间;
  • PEXPIRE 以毫秒为单位设置键的生存时间;
  • EXPIREAT 以秒为单位,设置键的过期 UNIX 时间戳;
  • PEXPIREAT 以毫秒为单位,设置键的过期 UNIX 时间戳。

1.3 过期键的清除

Redis 使用的过期键删除策略是惰性删除加上定期删除.

定期删除是这两种策略的一种折中:

  • 它每隔一段时间执行一次删除操作,并通过限制删除操作执行的时长和频率,籍此来减少删除操作对 CPU 时间的影响。(相当于定时执行一次删除,但是这个删除是有限制时间和频率的)
  • 另一方面,通过定期删除过期键,它有效地减少了因惰性删除而带来的内存浪费。

1.4 过期删除流程

实现过期键惰性删除策略的核心是 db.c/expireIfNeeded 函数——所有命令在读取或写入数据库之前,程序都会调用 expireIfNeeded 对输入键进行检查,并将过期键删除:

Redis 数据库

Get 获得数据:

Redis 数据库

1.4 过期键对 AOF 、 RDB 和复制的影响

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

在键已经过期,但是还没有被惰性删除或者定期删除之前,这个键不会产生任何影响, AOF 文
件也不会因为这个键而被修改。当过期键被惰性删除、或者定期删除之后,程序会向 AOF 文件追加一条 DEL 命令,来显式地
记录该键已被删除。

举个例子,如果客户端使用 GET message 试图访问 message 键的值,但 message 已经过期了,
那么服务器执行以下三个动作:

  1. 从数据库中删除 message ;
  2. 追加一条 DEL message 命令到 AOF 文件;
  3. 向客户端返回 NIL 。

2. RDB

SAVE 和 BGSAVE 命令实现 RDB 的功能实现。

2.1 RDB 文件结构

Redis 数据库

3. AOF

3.1 缓存追加

整个缓存追加过程可以分为以下三步:

  1. 接受命令、命令的参数、以及参数的个数、所使用的数据库等信息。
  2. 将命令还原成 Redis 网络通讯协议。
  3. 将协议文本追加到 aof_buf 末尾。

3.2 AOF 写入和保存

WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。

3.3 AOF 同步过程

同步命令到 AOF 文件的整个过程可以分为三个阶段:

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

3.4 AOF 保存模式

Redis 目前支持三种 AOF 保存模式,它们分别是:

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

每一秒钟保存一次

每当 flushAppendOnlyFile 函数被调用时,可能会出现以下四种情况:

  • 子线程正在执行 SAVE ,并且:

    • 这个 SAVE 的执行时间未超过 2 秒,那么程序直接返回,并不执行 WRITE 或新的SAVE 。
    • 这个 SAVE 已经执行超过 2 秒,那么程序执行 WRITE ,但不执行新的 SAVE 。注意,因为这时 WRITE 的写入必须等待子线程先完(旧的) SAVE ,因此这里 WRITE 会比平时阻塞更长时间。
  • 子线程没有在执行 SAVE ,并且:

    • 上次成功执行 SAVE 距今不超过 1 秒,那么程序执行 WRITE ,但不执行 SAVE 。
    • 上次成功执行 SAVE 距今已经超过 1 秒,那么程序执行 WRITE 和 SAVE 。可以用流程图表示这四种情况:

    Redis 数据库

根据以上说明可以知道,在“每一秒钟保存一次”模式下,如果在情况 1 中发生故障停机,那么用户最多损失小于 2 秒内所产生的所有数据。
Redis 官网上所说的, AOF 在“每一秒钟保存一次”时发生故障,只丢失 1 秒钟数据的说法,实际上并不准确

3.6 AOF 保存模式对性能和安全性的影响

  1. 不保存(AOF_FSYNC_NO):写入和保存都由主进程执行,两个操作都会阻塞主进程。
  2. 每一秒钟保存一次(AOF_FSYNC_EVERYSEC):写入操作由主进程执行,阻塞主进程。保存操作由子线程执行,不直接阻塞主进程,但保存操作完成的快慢会影响写入操作的阻塞时长。
  3. 每执行一个命令保存一次(AOF_FSYNC_ALWAYS):和模式 1 一样。

3.7 AOF 重写

是为了防止 AOF 越来越大,对多条命令合并成一个命令,例如:

1
2
3
4
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]

而是直接读取 list 键在数据库的当前值,然后用一条 RPUSH 1 2 3 命令来代替前面的四条命令

4. 事件

4.1 文件事件

读和写 事件

4.2 时间事件

定期需要执行的任务

例如:

  • 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。

  • 清理数据库中的过期键值对。

  • 对不合理的数据库进行大小调整。

  • 关闭和清理连接失效的客户端。

  • 尝试进行 AOF 或 RDB 持久化操作。

  • 如果服务器是主节点的话,对附属节点进行定期同步。

  • 如果处于集群模式的话,对集群进行定期同步和连接测试

4. 服务端和客户端

启动过程

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

执行完成后,各个组件的关系:

Redis 数据库

客户端连接到服务器

当一个客户端通过套接字函数 connect 到服务器时,服务器执行以下步骤:

  1. 服务器通过文件事件无阻塞地 accept 客户端连接,并返回一个套接字描述符 fd
  2. 服务器为 fd 创建一个对应的 redis.h/redisClient 结构实例,并将该实例加入到服务器的已连接客户端的链表中。
  3. 服务器在事件处理器为该 fd 关联读文件事件。