zhenlanghuo's Blog

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

2017/04/20

《Redis设计与实现》一书的重要内容摘抄,方便回看复习


1.数据库

要点

  • Redis 服务器的所有数据库都保存在 redisServer.db 数组中, 而数据库的数量则由 redisServer.dbnum 属性保存。
  • 客户端通过修改目标数据库指针, 让它指向 redisServer.db 数组中的不同元素来切换不同的数据库
  • 数据库主要由 dict 和 expires 两个字典构成, 其中 dict 字典负责保存键值对, 而 expires 字典则负责保存键的过期时间
  • 因为数据库由字典构成, 所以对数据库的操作都是建立在字典操作之上的。
  • 数据库的键总是一个字符串对象, 而值则可以是任意一种 Redis 对象类型, 包括字符串对象、哈希表对象、集合对象、列表对象和有序集合对象, 分别对应字符串键、哈希表键、集合键、列表键和有序集合键。
  • expires 字典的键指向数据库中的某个键而值则记录了数据库键的过期时间, 过期时间是一个以毫秒为单位的 UNIX 时间戳。
  • Redis 使用惰性删除定期删除两种策略来删除过期的键: 惰性删除策略只在碰到过期键时才进行删除操作, 定期删除策略则每隔一段时间, 主动查找并删除过期键。
  • 执行 SAVE 命令或者 BGSAVE 命令所产生的新 RDB 文件不会包含已经过期的键。
  • 执行 BGREWRITEAOF 命令所产生的重写 AOF 文件不会包含已经过期的键。
  • 当一个过期键被删除之后, 服务器会追加一条 DEL 命令到现有 AOF 文件的末尾, 显式地删除过期键。
  • 当主服务器删除一个过期键之后, 它会向所有从服务器发送一条 DEL 命令, 显式地删除过期键。
  • 从服务器即使发现过期键, 也不会自作主张地删除它, 而是等待主节点发来 DEL 命令, 这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
  • 当 Redis 命令对数据库进行修改之后, 服务器会根据配置, 向客户端发送数据库通知。

数据库.png-22.1kB

数据库实例.png-54.8kB

2.RDB持久化

要点

  • RDB 文件用于保存和还原 Redis 服务器所有数据库中的所有键值对数据。
  • SAVE 命令服务器进程直接执行保存操作,所以该命令会阻塞服务器
  • BGSAVE 命令子进程执行保存操作,所以该命令不会阻塞服务器
  • 服务器状态中会保存所有用 save 选项设置的保存条件(saveparams数组),当任意一个保存条件被满足时(使用dirty属性记录修改次数lastsave属性记录上一次执行保存的时间来判断是否满足保存条件),服务器会自动执行 BGSAVE 命令
  • 如果服务器开启了AOF持久化功能,服务器会优先使用AOF文件来还原数据库状态;只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
  • RDB 文件是一个经过压缩的二进制文件,由多个部分组成。
  • 对于不同类型的键值对, RDB 文件会使用不同的方式来保存它们。

3.AOF

要点

  • AOF 文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态。
  • AOF 文件中的所有命令都以 Redis 命令请求协议的格式保存
  • 命令请求会先保存到 AOF 缓冲区里面, 之后再定期写入并同步到 AOF 文件
  • appendfsync 选项的不同值(always、everysec、no)对 AOF 持久化功能的安全性、以及 Redis 服务器的性能有很大的影响。
  • 服务器只要载入并重新执行(通过创建一个不带网络连接的伪客户端来执行)保存在 AOF 文件中的命令, 就可以还原数据库本来的状态。
  • AOF 重写可以产生一个新的 AOF 文件, 这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样, 但体积更小
  • 由于Redis服务器使用单线程来处理命令请求,AOF重写是放到子进程里执行的。
  • AOF 重写是一个有歧义的名字, 该功能是通过读取数据库中的键值对来实现的, 程序无须对现有 AOF 文件进行任何读入、分析或者写入操作
  • 在执行 BGREWRITEAOF 命令时, Redis 服务器会维护一个 AOF 重写缓冲区, 该缓冲区会在子进程创建新 AOF 文件的期间, 记录服务器执行的所有写命令。 当子进程完成创建新 AOF 文件的工作之后, 服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾, 使得新旧两个 AOF 文件所保存的数据库状态一致。 最后, 服务器用新的 AOF 文件替换旧的 AOF 文件, 以此来完成 AOF 文件重写操作。
appendfsync 选项的值 flushAppendOnlyFile 函数的行为
always 将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件。
everysec 将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 如果上次同步 AOF 文件的时间距离现在超过一秒钟, 那么再次对 AOF 文件进行同步, 并且这个同步操作是由一个线程专门负责执行的。
no 将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 但并不对 AOF 文件进行同步, 何时同步由操作系统来决定。

文件的写入和同步
在现代操作系统中,调用write函数写数据到文件,实际上先写到内存缓冲区中,等到缓冲到达一定大小或者超过指定时限,才真正把缓冲区中的数据写到磁盘上。所以文件的写入是指写数据到缓冲同步是指将缓冲中的数据写入/刷新到磁盘

进行AOF后台重写时
进行AOF后台重写.png-22.5kB

4.事件

要点

  • Redis 服务器是一个事件驱动程序, 服务器处理的事件分为时间事件和文件事件两类。
  • 文件事件处理器是基于 Reactor 模式实现的网络通讯程序。
  • 文件事件是对套接字操作的抽象: 每次套接字变得可应答(acceptable)、可写(writable)或者可读(readable)时, 相应的文件事件就会产生。
  • 文件事件分为 AE_READABLE 事件(读事件)和 AE_WRITABLE 事件(写事件)两类。
  • 时间事件分为定时事件和周期性事件: 定时事件只在指定的时间达到一次, 而周期性事件则每隔一段时间到达一次。
  • 服务器在一般情况下只执行 serverCron 函数一个时间事件, 并且这个事件是周期性事件
  • 文件事件和时间事件之间是合作关系, 服务器会轮流处理这两种事件, 并且处理事件的过程中也不会进行抢占
  • 时间事件的实际处理时间通常会比设定的到达时间晚一些

文件事件

文件事件处理器

文件时间处理器.png-33.3kB

Redis 基于 Reactor 模式开发了自己的网络事件处理器: 这个处理器被称为文件事件处理器(file event handler)

  • 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

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

一次完整的客户端与服务器连接事件示例

客户端和服务器的通信过程.png-31.7kB

假设一个 Redis 服务器正在运作, 那么这个服务器的监听套接字的 AE_READABLE 事件应该正处于监听状态之下, 而该事件所对应的处理器为连接应答处理器

如果这时有一个 Redis 客户端向服务器发起连接, 那么监听套接字将产生 AE_READABLE 事件触发连接应答处理器执行: 处理器会对客户端的连接请求进行应答, 然后创建客户端套接字, 以及客户端状态, 并将客户端套接字的 AE_READABLE 事件与命令请求处理器进行关联, 使得客户端可以向主服务器发送命令请求。

之后, 假设客户端向主服务器发送一个命令请求, 那么客户端套接字将产生 AE_READABLE 事件引发命令请求处理器执行, 处理器读取客户端的命令内容, 然后传给相关程序去执行。

执行命令将产生相应的命令回复, 为了将这些命令回复传送回客户端, 服务器会将客户端套接字的 AE_WRITABLE 事件与命令回复处理器进行关联: 当客户端尝试读取命令回复的时候, 客户端套接字将产生 AE_WRITABLE 事件触发命令回复处理器执行, 当命令回复处理器将命令回复全部写入到套接字之后, 服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联。

事件的调度与执行

事件调度和执行的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def aeProcessEvents():

# 获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()

# 计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()

# 如果事件已到达,那么remaind_ms的值可能为负数,将它设定为0
if remaind_ms < 0:
remaind_ms = 0

# 根据remaind_ms的值,创建timeval结构
timeval = create_timeval_with_ms(remaind_ms)

# 阻塞并等待文件事件产生,最大阻塞时间由传入的timeval结构决定
# 如果remaind_ms的值为0,那么aeApriPoll调用之后马上返回,不阻塞
aeApiPoll(timeval)

# 处理所有已产生的文件事件
processFileEvents()

# 处理所有已到达的时间事件
processTimeEvents()


def main():

# 初始化服务器
init_server()

# 一直处理事件,直到服务器关闭为止
while server_is_not_shutdown():
aeProcessEvents()

# 服务器关闭,执行清理操作
clean_server()

5.客户端

要点

  • 服务器状态结构使用 clients 链表连接起多个客户端状态, 新添加的客户端状态会被放到链表的末尾。
  • 客户端状态的 flags 属性使用不同标志来表示客户端的角色, 以及客户端当前所处的状态。
  • 输入缓冲区记录了客户端发送的命令请求, 这个缓冲区的大小不能超过 1 GB 。
  • 命令的参数和参数个数会被记录在客户端状态的 argv 和 argc 属性里面, 而 cmd 属性则记录了客户端要执行命令的实现函数。
  • 客户端有固定大小缓冲区和可变大小缓冲区两种缓冲区可用, 其中固定大小缓冲区的最大大小为 16 KB , 而可变大小缓冲区的最大大小不能超过服务器设置的硬性限制值。
  • 输出缓冲区限制值有两种, 如果输出缓冲区的大小超过了服务器设置的硬性限制, 那么客户端会被立即关闭; 除此之外, 如果客户端在一定时间内, 一直超过服务器设置的软性限制, 那么客户端也会被关闭。
  • 当一个客户端通过网络连接连上服务器时, 服务器会为这个客户端创建相应的客户端状态。 网络连接关闭、 发送了不合协议格式的命令请求、 成为 CLIENT_KILL 命令的目标、 空转时间超时、 输出缓冲区的大小超出限制, 以上这些原因都会造成客户端被关闭。
  • 处理 Lua 脚本的伪客户端在服务器初始化时创建, 这个客户端会一直存在, 直到服务器关闭。
  • 载入 AOF 文件时使用的伪客户端在载入工作开始时动态创建, 载入工作完毕之后关闭。

6.服务器

要点

  • 一个命令请求从发送到完成主要包括以下步骤: 1. 客户端将命令请求发送给服务器; 2. 服务器读取命令请求,并分析出命令参数; 3. 命令执行器根据参数查找命令的实现函数,然后执行实现函数并得出命令回复; 4. 服务器将命令回复返回给客户端。
  • serverCron 函数默认每隔 100 毫秒执行一次, 它的工作主要包括更新服务器状态信息, 处理服务器接收的 SIGTERM 信号, 管理客户端资源和数据库状态, 检查并执行持久化操作, 等等。
  • 服务器从启动到能够处理客户端的命令请求需要执行以下步骤: 1. 初始化服务器状态; 2. 载入服务器配置; 3. 初始化服务器数据结构; 4. 还原数据库状态; 5. 执行事件循环。
CATALOG
  1. 1. 1.数据库
    1. 1.1. 要点
  2. 2. 2.RDB持久化
    1. 2.1. 要点
  3. 3. 3.AOF
    1. 3.1. 要点
  4. 4. 4.事件
    1. 4.1. 要点
    2. 4.2. 文件事件
      1. 4.2.1. 文件事件处理器
      2. 4.2.2. 一次完整的客户端与服务器连接事件示例
    3. 4.3. 事件的调度与执行
  5. 5. 5.客户端
    1. 5.1. 要点
  6. 6. 6.服务器
    1. 6.1. 要点