• 已删除用户
Administrator
发布于 2021-06-30 / 526 阅读
0

redis单机数据库的实现

Redis单机数据库的实现

一、数据库

1.1服务器中的数据库

struct redisServer{
    //服务器的数据库数量,默认为16
    int dbnum;
    //一个数据,保存着服务器中的所有数据库
	redisDb *db;
}

image-20210628153141047

1.2切换数据库

每个redis客户端都有自己的目标数据库,可以通过select命令来切换目标数据库;

在服务器内部,客户端状态的redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针;

通过修改redisClient.db指针,让它指向服务器中的不同数据库,从而实现切换目标数据库的功能,这就是select命令的实现原理;

1.3数据库键空间

Redis是一个键值对数据库服务器,服务器中的每个数据库都由一个redis.h/redisDb结构表示,其中,redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间;

typedef struct redisDb{
    //数据库键空间,保存着数据库中的所有键值对
	dict *dict;
}redisDb;

键空间和用户所见的数据库是直接对应的:

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

②键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的任意一种Redis对象;

image-20210628155428864

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

通过expire命令或者pexpire命令,客户端可以以秒或者毫秒的精度为数据库中的某个键设置生存时间,在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为0的键;

1.4.1设置过期时间

①expire命令用于将键key的生存时间设置为ttl秒;

②pexpire命令用于将键key的生存时间设置为ttl毫秒;

③expireat命令用于将键的过期时间设置为timestamp所指定的秒数时间戳;

④pexpireat命令用于将键的过期时间设置为timestamp所指定的毫秒数时间戳;

无论客户端执行的是以上四个命令中的哪一个,经过转换之后,最终的执行效果都和pexpireat命令一样;

1.4.2保存过期时间

redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称字典为过期字典:

①过期字典的键是一个指针,这个指针指向键空间中的某个键对象;

②过期字典的值是一个long long类型的整数,这个整数保存了所有指向数据库键的过期时间;

image-20210628160744442

1.4.3移除过期时间

persist命令可以移除一个键的过期时间;

1.4.4计算并返回剩余生成时间

ttl命令以秒为单位返回键的剩余生存时间,而pttl命令以毫秒为单位返回键的剩余生存时间;

1.4.5过期键的判定

检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间,检查当前linux时间戳是否大于键的过期时间:如果是,那么键已过期;否则,键未过期;

1.5过期键删除策略

1.5.1定时删除

定时删除策略对内存是最友好的:通过使用定时器,定时删除策略可以保证过期键会尽可能快地被删除,并释放过期键所占用地内存;另一方面,定时删除策略的缺点是,它对CPU时间是最不友好的;

1.5.2惰性删除

惰性删除策略对CPU时间来说是最友好的:程序只会在取出键时才对键进行过期检查,这可以保证删除过期键的操作只会在非做不可的情况下进行;缺点是:对内存最不友好;

1.5.3定期删除

定期删除策略是前两种策略的一种整合和折中;但是,如果执行太频繁,会退化成定时删除策略,如果执行太少,又会退化成惰性策略;

1.6Redis的过期键删除策略

1.6.1惰性删除策略的实现

image-20210628162627627

1.6.2定期删除策略的实现

过期键的定时删除策略由redis.c/activeExpireCycle函数实现,每当该函数被调用,它在规定时间内,分多次遍历服务器中的各个数据库,检查并删除过期键;

二、RDB持久化

Redis是内存数据库,它将自己的数据库状态储存在内存里面,所以如果不想办法储存在内存中的数据库状态保存到磁盘里面,那么一旦服务进程退出,服务器的数据库状态也会消失不见;

2.1RDB文件的创建与载入

有两个命令可以用于生存RDB文件,一个是save,另一个是bgsave;

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

和save命令直接阻塞服务器进程的做法不同,bgsave命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求;

服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止;

2.2自动间隔性保存

save 900 1
save 300 10
save 60 10000

只要满足以下三个条件中任意一个,bgsave命令就会被执行;

2.2.1dirty计数器和lastsave属性

①dirty计数器记录距离上一次成功执行save命令或者bgsave命令之后,服务器对数据库状态进行了多少次修改;

②lastsave属性是一个时间戳,记录了服务器上一次成功执行save命令或者bgsave命令的时间;

2.2.2检查保存条件是否满足

Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,其中一项工作就是检查save选项的保存条件是否已经满足,如果满足,就执行bgsave命令;

三、AOF持久化

AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的;

3.1AOF持久化的实现

AOF持久化功能的实现可以分为命令追加(append),文件写入,文件同步(sync)三个步骤;

3.1.1命令追加

struct redisServer{
	//AOF缓冲区
    sds aof_buf;
}

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

3.1.2AOF文件的写入与同步

Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。

服务器在执行写命令后,命令会被追加到缓冲区,所以服务器在每次结束一个事件循环之前,都会调用flushAppendOnlyFile函数,考虑是否需要将缓冲区内容写入和保存到AOF文件里面。

image-20210629133525768

如果用户没有主动设置appendfsync的值,默认为everysec。

3.2AOF文件的载入与数据还原

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

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

①创建一个不带网络连接的伪客户端:因为redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接;

②从AOF文件中分析并读取出一条写命令;

③使用伪客户端执行被读出的写命令;

④一直执行步骤②和步骤③,直到AOF文件中的所有写命令被处理完毕为止;

3.3AOF重写

因为AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF文件会越来越多。

为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令。

3.3.1AOF后台重写

①子进程进行AOF重写期间,服务器进程可以继续处理命令请求;

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

不过,使用子进程也有一个问题,因为子进程在进行AOF重写期间,服务进程还需要继续处理命令请求,新的命令可能会对现有的服务器状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致;

为了解决这种数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区;

当子进程完成AOF重写工作之后,它会向父进程发送一个信号,执行以下工作:

①AOF缓冲区的内容会定期被写入和同步到AOF文件,现有工作照旧;

②创建子进程开始,服务器执行的所有命令都会被记录到AOF缓冲区里面;

当子进程完成AOF重写工作后,AOF重写缓冲区中的所有内容写入到新AOF文件中,对新的AOF文件进行改名,原子地覆盖现有地AOF文件,完成新旧两个AOF文件替换;

四、事件

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

①文件事件:Redis服务器通过套接字与客户端进行连接,而文件事件就是服务器对套接字操作地抽象;

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

4.1文件事件

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

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

4.1.1文件事件处理器的构成

每当一个套接字准备好执行连接应答、写入、读取、关闭等操作时,就会产生一个文件事件。尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生的事件都放到一个队列里面,然后以有序,同步,每次一个套接字的方式向文件事件分派器传送套接字。

image-20210629180433220

4.1.2文件事件处理器

①连接应答处理器:主要用于对连接服务器监听套接字的客户端进行应答,当Redis服务器初始化时,程序会将这个连接应答处理器和服务器监听套接字的AE_READABLE事件关联起来;

②命令请求处理器:负责从套接字中读入客户端发送的命令请求内容,当一个客户端通过连接应答处理器成功连接服务器之后,服务器会将客户端套接字的AE_READABLE事件和命令请求处理器关联;

③命令回复处理器:当服务器有命令回复需要传送给客户端的时候,服务器会将客户端套接字的AE_WRITABLE事件和命令回复处理器关联起来,当客户端准备好接收服务器传回的命令回复时,就会产生AE_WRITABLE事件,引发命令回复处理器执行;

image-20210630102335878

4.2时间事件

Redis的时间事件分为两类:定时事件、周期性事件;

4.2.1实现

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

4.2.2事件的调度与执行

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()

将aeProcessEvents函数置于一个循环里面,加上初始化和清理函数,就构成了Redis服务器的主函数:

def mian():
	#初始化服务器
	init_server()
    #一直处理事件,直到服务器关闭为止
    while server_is_not_shutdown():
		aeProcessEvents()
    #服务器关闭,执行清理操作
    clean_server()  

以下是事件的调度和执行规则:

①aeApiPoll函数的最大阻塞时间由到达时间最接近当前时间的时间事件决定,这个方法既可以避免服务器对事件事件进行频繁的轮询,也可以确保aeApiPoll函数不会阻塞过长时间;

②因为文件事件是随机出现的,如果等待并处理完一次文件事件之后,仍未有任何时间事件到达,那么服务器将再次等待并处理文件事件。随着文件事件的不断执行,时间会逐渐逼进;

③对文件事件和时间事件的处理都是同步、有序、原子地执行,服务器不会中断事件处理,也不会对事件进行抢占;

④因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一点;