Redis 第二部分 “单机数据库的实现”

数据库

Redis 服务器存在多个数据库,默认情况下会创建16个数据库。

默认情况下,Redis 客户端的目标数据库为 0 号数据库,但是可以通过执行 SELECT 命令来切换目标数据库。

到目前为止,Redis 仍然没有可以返回客户端目标数据库的命令。为了避免对数据库进行误操作,所以在执行 Redis 命令前,先执行 SELECT 命令。

Redis 是一个键值对数据库服务器,内部结构中 redisDb 结构的 dict 字典保存了数据库中的所有键值对,这个字典我们称为键空间。

键空间的键也是数据库的键,每个键都是一个字符串对象。

键空间的值也是数据库的值,它可以是 任意一种 Redis 对象。

Redis 对某个数据库进行指令操作时,例如 新增,删除操作等,都是通过键空间进行操作。

读写键空间时的维护操作

当使用 Redis 命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作:

读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中次数或键空间不命中次数。

读取一个键之后,服务器会更新键的LRU 时间,这个值可以用于计算键的闲置时间。

如果服务器在读取一个键时发现已经过期,那么服务器会先删除这个过期键,然后再执行余下的其他操作。

客户端使用 WATCH 命令监视了某个键,那么服务器再对被监视的键进行修改过后,这个键会被标记 dirty,这是为了让事务程序注意到这个键已经被修改过。

服务器每修改一个键,都会对 dirty 键计数器的值增1,这个计数器会触发服务器的持久化及复制操作。

设置键的生存时间或过期时间

通过EXPIRE 或者 PEXPIRE 命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间,服务器会自动删除生存时间为 0 的键。

SETEX命令设置过期时间和 EXPIRE命令设置过期时间的原理是完全一样的。

EXPIRE 命令用于将键 key 的生存时间设置为 ttl 秒。

PEXPIRE 命令用于将键 key 的生存时间设置为 ttl 毫秒。

PERSISR 命令可以移除一个键的过期时间。

TLL 命令返回以秒为单位的键剩余生存时间,PTTL命令则以毫秒为单位返回键剩余生存时间。

过期键删除策略

定时删除:

优点:通过定时器,定时删除可以保证过期键会尽可能快的被删除,并释放过期键所占用的内存。

缺点:会占用相当一部分CPU时间,会对服务器的响应时间和吞吐量造成影响。

惰性删除:

优点:程序只会再取出键时才会对键进行过期检查,这样可以保证删除过期键的操作只会在非做不可的情况下进行,并且删除的目标仅限于当前处理的键,所以这个策略不会删除其他物馆的过期键而导致浪费多余的CPU时间。

缺点:如果一个键已经过期,而这个键又仍然保留在数据库中,那么它会一直占用内存而得不到释放。一旦无用的辣鸡数据占用了大量的内存,但是却得不到释放,造成后果肯定非常严重。

定期删除:

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

定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。

除此之外,通过定期删除过期键,定期删除策略有效减少内存浪费。

如果删除操作执行的太频繁,或执行时长过于长,定期删除策略就会退化成定时删除策略。

如果执行的太少,或者执行时间太短,那么就会出现内存浪费的情况。

服务器必须根据自身情况,合理设置删除操作的执行时长和执行频率。

数据库通知

数据库通知同能可以让客户端通过订阅给定的频道或模式,来获知数据库中键的边华,以及数据库中命令的执行情况。

1
2
3
4
5
6
服务器配置的 notify-keyspace-events 选项决定了发送通知的类型:
服务器发送所有类型的键空间通知和键事件通知,设置为 AKE
服务器发送所有类型的键空间通知,设置为 AK
服务器发送所有类型的键事件通知,设置为 AE
服务器只发送和字符串键有关的键空间通知,设置为 K$
服务器只发送和列表键有关的键事件通知,设置为 E1

RDB 持久化

因为Redis 是内存数据库,它的数据是存在内存里面的。但是Redis 也提供 RDB 持久化功能,它可以将数据库状态保存到磁盘中,避免数据意外丢失。

RDB 持久化既可以手动执行,也可以根据服务器配置选项定期执行,它能将某个时间点上的数据库状态保存到一个 RDB 文件中。

RDB 是一个经过压缩的二进制文件。

RDB 文件的创建与载入

两个命令可以可以用于生成 RDB 文件: SAVE , BGSAVE

SAVE命令会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕为止,阻塞期间服务器不能处理任何命令请求。

BGSAVE 会派生出一个子进程,然后由子进程创建 RDB 文件,服务器主进程进程继续处理请求。

**RDB文件的载入工作是在服务器启动时自动执行的,所以只要 Redis 服务器在启动时检测到 RDB 文件存在,它就会自动载入 RDB 文件。 **

因为AOF 文件的更新频率通常比 RDB 文件的更新频率更高,所以如果服务器开启了 AOF 持久化,那么服务器会优先使用 AOF 文件来还原数据库状态。

只有在 AOF 关闭时,服务器才会使用 RDB 文件来还原数据库。

RDB 文件在载入时是一个单一的过程,在执行了 BGSAVE 命令后,无论是再向服务器发送 SAVA 命令或 BASAVE 命令,Redis 都会拒绝。

但是如果 BGSAVE 命令正在执行,BGREWRITEAOF 命令会被延迟到 BGSAVE 命令执行完毕之后执行。

如果 BGREWRITEAOF 命令正在执行,发送 BGSAVE 命令会被拒绝。

自动间隔性保存

Redis 允许用户通过设置服务器配置的 sava 选项,让服务器每隔一段时间就执行一次 BGSAVE 命令。同时,用户也可以设置多个保存条件,只要启动任意一个条件被满足,服务器就会执行 BGSAVE 命令。

1
2
3
sava 999 1      #服务器在900秒内,对数据库进行了至少一次修改。
save 300 10 #服务器在300秒内,对数据库进行了至少十次修改。
save 60 10000 #服务器在60秒内,对数据库进行了至少一万次修改。

AOF 持久化

与 RDB 持久化通过保存数据库中的键值对来记录数据库状态不同,AOF 持久化是通过保存 Redis 服务器所执行的写命令来记录数据库状态。

AOF 持久化功能可以分为三个步骤:

命令追加

AOF 持久化功能处于打开状态时,服务器在执行完一个命令之后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾

写入与同步

服务器在处理文件事件时,可能会执行写命令,每次结束一个事件循环之前,服务器会考虑是否将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件里面。

1
2
3
always  		将 aof_buf 缓冲区中的所有内容写入并同步到AOF文件
everysec 将 aof_buf 缓冲区中的所有内容写入并同步到AOF文件,如果上次同步 AOF 文件的事件距离限制超过一秒,那么再次对 AOF 文件进行同步,这个同步操作会另起一个线程专门负责执行。
no 将 aof_buf 缓冲区中的所有内容写入并同步到AOF文件,但是不对 AOF 文件进行同步,由操作系统来决定什么时候同步。

如果没有设置选项,默认是 everysec。

服务器配置 appendfsync 选项的值直接决定 AOF 持久化功能的效率和安全性。

always 的效率是最慢的一个,但是最安全,即使出现故障停机,AOF 持久化也只会丢失一个事件循环中所产生的命令数据。

everysec 模式足够快,并且就算出现故障停机,数据库也只丢失一秒钟的命令数据。

no 模式下 AOF 文件写入速度总是最快的,但是同步时长最长。当出现故障停机时,会丢失上次同步 AOF 文件之后的所有写命令数据。

文件载入与数据还原

因为 AOF 文件里面包含了重建数据库状态所有写命令,所以服务器只要读取文件并重写执行一遍 AOF 文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。

Redis 读取 AOF 并还原数据库状态步骤:

1.创建一个不带网络连接的伪客户端,因为 Redis 的命令只能在客户端上下文中执行,所以需要服务器使用一个没有网络连接的伪客户端执行 AOF 的写命令。

2.从 AOF 读取一条写命令

3.使用伪客户端执行这条写命令

4.重复2和3步,直到所有命令执行完毕

AOF 重写

随着时间流逝,AOF的文件会越来越大,为了解决这一问题,Redis 提供了 AOF 重写功能。通过重写,Redis 服务器可以创建一个新的 AOF 文件来替代现有的 AOF 文件,新旧两个 AOF 文件所保存的数据库状态相同,但是新 AOF 文件的体积通常比旧文件体积要小得多。

Redis 将 AOF 重写程序放在子进程中执行:

1.子进程进行 AOF 重写期间,服务器主进程可以继续处理命令请求。

2.子进程带有服务器进程的数据副本,使用子进程而不是线程,可以避免使用锁的情况下,保证数据的安全性。

Redis 服务器设置了一个 AOF 重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当 Redis 服务器执行完一个写命令之后,同时会将这个写命令发送给 AOF 缓冲区和重写缓冲区。

这样可以保证,AOF 缓冲区的内容会定期被写入和同步到 AOF 文件,对现有的 AOF 文件处理工作会照常进行。

同时,创建子进程开始,服务器执行的所有写命令都会被记录到 AOF 重写缓存区里面。

当子进程完成 AOF 重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会将AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这时新 AOF 文件与服务器数据库状态一致。然后对新的 AOF 文件进行改名,覆盖现有的 AOF 文件,完成新旧文件替换。

事件

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

1.文件事件:Redis 服务器通过套接字与客户端进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端的通信会产生相应的文件事件,而服务器则通过监听处理这些事件来完成一系列网络通信操作。

时间事件:Redis 服务器中的一些操作需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。

文件事件

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

文件事件处理器使用 I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。

当被监听的套接字准备好执行应答,读取,写入,关闭等操作时,与操作相对应的文件事件就产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

Redis 使用 I/O多路复用程序来监听多个套接字。

时间事件

定时事件:让一段程序在指定的时间之后执行一次。

周期性事件:让一段程序每隔指定事件就执行一次。

服务器一般情况下只执行 serverCron 函数只一个时间事件,并且这个事件是周期性事件。

时间事件的实际处理时间通常会比设定的到达时间晚一点些。

文件事件和时间事件是合作关系,处理事件的过程中不会发生抢占。

客户端

Redis 服务器是一对多服务器程序,可以与多个客户端建立网络连接。服务器会使用一个链表保存所有与服务器连接的客户端状态结构。

在主从服务器进行复制操作时,主服务器会成为从服务器的客户端,而从服务器也会成为主服务器的客户端。

Redis 只会将那些对数据库进行了修改的命令写入到 AOF 文件,并复制到各个从服务器。如果一个命令没有对数据库进行任何修改,那么这个命令不会被写入到 AOF 文件,也不会被复制到从服务器。

但是 PUBSUB 和 SCRIPT LOAD 命令是其中的列外。

服务器状态结构使用链表连接多个客户端状态,新添加的客户端状态会被放到链表的末尾。

客户端状态的 flags 属性使用不同标志来表示客户端的角色,以及客户端当前所处的状态。

输入缓存区记录了客户端发送的命令请求,但是缓存区的大小不能超过 1 GB。

客户端分为固定缓冲区和可变大小缓存区,其中固定大小缓冲区最大为 16KB,可变大小缓冲区的最大大小不能超过服务器设置的硬性限制值。

输出缓冲区限制值有两种,如果输出缓冲区的大小超过了服务器设置的硬性限制,客户端会被立即关闭;如果客户端在一定时间内,一直超过服务器设置的软性限制,客户端也会被关闭。

网络连接关闭,发送不符合协议格式的命令请求,成为 CLIENT KILL 命令的目标,空转时间超时,输出缓冲区的大小超出限制,都会照成客户端被关闭。

处理 Lua 脚本的伪客户端在服务器初始化时创建,这个客户端会一直存在,直到服务器关闭。

载入 AOF 文件使用的伪客户端在载入工作开始时动态创建,载入工作完毕之后关闭。

学习资料

《Redis 设计与实现》