β

Redis设计与实现总结——单机数据库的实现

oohcode 7 阅读

数据库

一个Redis Server可以有多个Redis数据库,这点类似于MySQL, 从Redis Server的源代码中可以看到, redisDb 是Server数据库的指针,指向一个数据库组成的数组,而数据库的数量则由 dbnum 属性来表示。客户端可以通过 SELECT 命令选择当前要操作的数据库。

struct redisServer {

// ...

// 数据库数组指针
redisDb *db;

// 服务器的数据库数量
int dbnum;

// ...
}

数据库的定义在 redis.h/redisDb 中,定义如下:

typedef struct redisDb {

// 数据库键空间,保存着数据库中的所有键值对
dict *dict; /* The keyspace for this DB */

// 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
dict *expires; /* Timeout of keys with a timeout set */

// 正处于阻塞状态的键
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */

// 可以解除阻塞的键
dict *ready_keys; /* Blocked keys that received a PUSH */

// 正在被 WATCH 命令监视的键
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */

struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */

// 数据库号码
int id; /* Database ID */

// 数据库的键的平均 TTL ,统计信息
long long avg_ttl; /* Average TTL, just for stats */

} redisDb;

一个简化的结构图如下:
db结构
设置生存时间和过期时间时,最终都是计算出最后生存时间,然后把这个值存入 expires 字典中。过期字典中找不到证明没有设置过期时间。过期删除策略Redis主要是使用惰性删除策略与定期删除两种策略。所谓惰性删除策略就是当用户获取键时,先判断其是否过期,如果过期则删除键,返回失败,如果没过期则正常返回。定期删除策略是Redis会周期行的从过期字典中随机出一部分键值,如果过期则删除键,否则保留。

持久化

RDB持久化

RDB(redis database)持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中(RDB文件默认的文件名为 dump.rdb )。RDB持久化功能锁生成的RDB文件是一个经过压缩的二进制文件,通过该文件还可以还原生成RDB文件时的数据库状态。
有两个Redis命令可以用于生成RDB文件,一个是 SAVE , 另一个是 BGSAVE SAVE 会阻塞Redis服务进程,知道RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何请求。 BGSAVE 命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。
RDB文件是在服务器启动时自动执行的,只要Redis服务器启动时检测到RDB文件存在,它就会自动载入RDB文件。但是如果服务器开启了AOF持久化功能,就会优先使用AOF文件。因为AOF文件的更新频率通常比RDB文件高,所以数据是最新的可能性高。
用户可以通过save选项设置多个保存条件,但只要其中任意一条被满足,服务器就会执行 BGSAVE 命令。例如配置为下面三个:

save 900 1
save 300 10
save 60 10000

只要满足900s内至少一次修改,或300s内至少10次修改,或60s内10000次修改就会自动执行 BGSAVE 命令。
服务器维护一个 dirty 计数器,用于记录距离上次成功执行 SAVE BGSAVE 命令之后,服务器对数据库状态进行了多少次修改(包括写入,删除,更新等操作)。
服务器还维护一个 lastsave 属性,记录服务器上一次成功执行 SAVE BGSAVE 命令的时间。
RDB文件结构:

+-----+----------+---------+---+---------+
| | | | | |
|REDIS|db_version|databases|EOF|check_sum|
| | | | | |
+-----+----------+---------+---+---------+

AOF持久化

AOF(Append Only File)持久化功能是通过保存Redis服务器所执行的写命令来记录数据库状态的。AOF持久化功能的实现可以分为命令追加(append), 文件写入,文件同步(sync)三个步骤:

appendfsync选项的值 flushAppendOnlyFile函数的行为 影响
always 将aof_buf缓冲区中的所有内容写入并同步到AOF文件 性能最低,但是安全性最高,发生故障停机最多丢失一个循环事件所产生的在缓冲区中的命令
everysec(默认值) 将aof_buf缓冲区中的所有内容写入到AOF文件,如果上次同步AOF文件的时间距离现在超过1s,那么再次对AOF文件进行同步,并且这个同步操作是由一个线程专门负责执行的 性能足够快,并且出现故障停机,最多丢失一秒钟的命令数据
no 将aof_buf缓冲区中的所有内容写入到AOF文件,但并不对AOF文件进行同步,何时同步由操作系统决定 性能最好,写入AOF速度最快,但是单次同步时间最长,出现故障丢失的命令最多

由于AOF文件记录了重建数据库所需的所有写命令,所以服务器只要读入并执行一遍AOF文件里么保持的写命令,就可以还原服务器关闭之前的状态。
由于AOF持久化是通过保存被执行的写命令来记录数据库状态的,随着时间的推移,写命令越来越多,这时候就需要 AOF重写 来减轻文件体积的膨胀。
AOF重写 首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录的这个键值对的多条命令。但是在重写列表,哈希表,集合,有序集合等多个元素的键时,如果元素的数量超过了 redis/REDIS_AOF_REWRITE_ITEMS_PER_CMD 常量的值,会通过多条命令来记录键的值。
一个问题是在AOF重写期间,服务器还需要处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。为了解决这个问题,Redis服务器设置了一个 AOF重写缓冲区 ,这个缓冲区在服务器创建子进程进行重写是开始使用,当Redis服务器执行完一个写命令后,它会同事将这个命令发送给AOF缓冲区和 AOF重写缓冲区 。当AOF重写工作完成后,向父进程发送信号,父进程就会将 AOF重写缓冲区 中的所有内容写到新的AOF文件中,对新的AOF文件进行改名,原子地 (atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换。

事件

文件事件

文件事件(file event): Redis服务器通过套接字与客户端(或其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
下图是Redis自己实现的文件事件处理器的四个组成部分:
db结构

虽然文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接,着保持了Redis内部单线程设计的简单性。
尽管多个文件事件可能会并发地出现,但I/O多路复用程序总会将所有产生事件的套接字都放在一个队列里,然后通过这个队列,以有序(sequentially),同步(synchronously),每次一个套接字的方式向文件事件分派器传送套接字。
Redis的I/O多路复用程序的所有功能都是通过包装常见的 select , epoll , evport kqueue 这些I/O多路复用函数库来实现的,编译时会自动选择性能高最高的I/O多路复用函数库来作为Redis的I/O多路复用程序的底层实现。

时间事件

时间事件(time event): Redis服务器中的一些操作(如 serverCron 函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。
Redis的时间事件分为两类:

一个时间事件主要由以下三个属性:

一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值:

服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

事件的调度与执行

事件的调度和执行由 ae.c/aeProcessEvents 函数负责,下面是这个函数的伪代码:

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,那么aeApiPoll调用之后马上返回,不阻塞
aeApiPoll(timeval)

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

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

事件的调度和执行规则:

  1. aeApiPoll函数的最大阻塞时间由到达时间最接近的当前时间的时间事件决定,这个方法既可以避免服务器对时间事件并行频繁的轮询,可以确保aeApiPoll函数不会阻塞时间过长。
  2. 因为文件事件是随机出现的,如果处理完文件事件后时间事件仍未到达,继续等待并处理下一个文件事件。
  3. 对文件事件和时间事件的处理都是同步,有序,原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占。因此耗时的事件会影响整个服务的性能。
  4. 因为时间事件是在文件事件之后执行,并且事件之间不会抢占,所以时间事件的实际处理时间通常回避时间事件设定的到达时间稍微晚一些。

客户端

通过使用I/O多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。
关于 redisClient 的定义可以从 redis.h 中看到,客户端有很多属性。这些属性可以分为两类:

属性

执行命令所得的命令回复会被保存到客户端状态的输出缓冲区里,每个客户端都有两个输出缓冲区可用

创建与关闭

服务器

命令请求的执行过程

前面讲了,客户端发送的请求会被放到输入缓冲区,然后服务器对命令进行解析,转换成协议格式,服务器将通过调用命令执行器来完成余下的步骤:

redisCommand 结构的主要属性:

属性名 类型 作用
name char * 命令的名字,比如”set”
proc redisCommandProc * 函数指针,指向命令的实现函数
arity int 命令参数的个数,用于检查命令请求的格式是否正确
sflags char * 字符串形式的标识值,这个值记录了命令的属性
例如:
w:表示写入命令
r:只读命令
m:可能会占用大量内存的命令
a:这是一个管理命令
flags int 对sflags标识进行分析得出的二进制标识,由程序自动生成
calls long long 服务器总共执行了多少次这个命令
milliseconds long long 服务器执行这个民两个所耗费的总时长

回复发送完毕后,回复处理器会清空客户端状态的输出缓冲区,未处理下一个命令请求做好准备。当客户端接收到协议格式的命令回复后,它会将这些回复转换成人类可读的格式,并打印给用户观看。

serverCron函数

Redis服务器中的 serverCron 函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并保持服务器自身的良好运转。 serverCron 的函数主要功能如下面所列:

初始化服务器

一个Redis服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程。过程如下:

数据库

一个Redis Server可以有多个Redis数据库,这点类似于MySQL, 从Redis Server的源代码中可以看到,

作者:oohcode
原文地址:Redis设计与实现总结——单机数据库的实现, 感谢原作者分享。

发表评论