8.redis学习笔记-AOF持久化&事件.md

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jun8148/article/details/82915676

10. AOF持久化

区分下RDB和AOF:

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

10.1. AOF持久化的实现

三个步骤:

  • 命令追加
  • 文件写入
  • 文件同步

10.1.1. 命令追加

前提条件:AOF持久化功能处于开启状态

当服务器执行完一个写命令之后,就会以协议格式将被执行的写命令****追加到服务器状态的aof_buf缓冲区**的末尾。

aof_buf的定义:

struct redisServer{
    // ...
    // AOF缓冲区
    sds aod_buf;
    // ...
};

10.1.2. AOF文件的写入和同步

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

当服务器在处理文件事件时,可能会执行写命令,所以会由内容被增加到aof_buf缓冲区末尾,所以服务器的每一次循环结束之前,都会调用flushAppendOnlyFile函数,考虑是否将aof_buf缓冲区的内容写入和保存到AOF文件中,过程伪代码:

def eventLoop():
	while True:
		# 处理文件事件,接收命令请求以及发送命令回复
		# 处理命令请求是可能会有新内容被追加到aof_buf缓冲区中
		processFileEvents();
		
		# 处理时间事件
		processTimeEvents();
		
		# 考虑是否要将aof_buf中的内容写入和保存到AOF文件中
		flushAppendOnlyFile();

flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来决定,各个不同值产生不同的行为:

appendfsync选项的值 flushAppendOnlyFile函数的行为
always aof_buf缓冲区的所有内容写入并同步到AOF文件中
everysec(默认值) aof_buf缓冲区中的所有内容写入到AOF文件中,如果上次同步AOF文件的时间距离现在超过一秒钟,那么再次对AOF文件进行同步,并且这个同步操作是由同一个线程专门负责执行的
no aof_buf缓冲区的所有内容写入到AOF文件中,但不对AOF文件进行同步,何时同步由操作系统决定

文件的写入和同步

为了提高文件的写入效率, 在现代操作系统中, 当用户调用 write 函数, 将一些数据写入到文件的时候, 操作系统通常会将写入数据暂时保存在一个内存缓冲区里面, 等到缓冲区的空间被填满、或者超过了指定的时限之后, 才真正地将缓冲区中的数据写入到磁盘里面。
这种做法虽然提高了效率, 但也为写入数据带来了安全问题, 因为如果计算机发生停机, 那么保存在内存缓冲区里面的写入数据将会丢失。
为此, 系统提供了 fsyncfdatasync 两个同步函数, 它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面, 从而确保写入数据的安全性。

举个例子, 假设服务器在处理文件事件期间, 执行了以下三个写入命令:

  1. SADD databases "Redis" "MongoDB" "MariaDB"
  2. SET date "2013-9-5"
  3. INCR click_counter 10086

那么 aof_buf 缓冲区将包含这三个命令的协议内容:

*5\r\n$4\r\nSADD\r\n$9\r\ndatabases\r\n$5\r\nRedis\r\n$7\r\nMongoDB\r\n$7\r\nMariaDB\r\n
*3\r\n$3\r\nSET\r\n$4\r\ndate\r\n$8\r\n2013-9-5\r\n
*3\r\n$4\r\nINCR\r\n$13\r\nclick_counter\r\n$5\r\n10086\r\n

如果这时 flushAppendOnlyFile 函数被调用, 假设服务器当前 appendfsync 选项的值为 everysec , 并且根据 server.aof_last_fsync 属性显示, 距离上次同步 AOF 文件已经超过一秒钟, 那么服务器会先将 aof_buf 中的内容写入到 AOF 文件中, 然后再对 AOF 文件进行同步。

10.2. AOF文件的载入和数据还原

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

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

  1. 创建一个不带网络连接的伪客户端:因为Redis的命令只能在客户端上下文执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用一个伪客户端来执行AOF文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行的命令的效果完全一样
  2. 从AOF文件中分析并读取出一条写命令
  3. 使用伪客户端执行被读出的写命令
  4. 重复步骤2、步骤3知道AOF文件的所有写命令被处理完为止

10.3. AOF重写

AOF持久化文件会保存写命令,而服务器运行的时间越长,AOF文件中的内容就会越来越多,文件的体积会越来越大,需要处理这些体积过大的AOF文件,不然会造成很大影响,并且AOF文件的还原需要花费更多的时间。

Redis中提供AOF文件重写功能,创建一个新的AOF文件替代现有的AOF文件,新旧文件保存的数据库状态相同,新的文件不包含任何浪费空间的冗余命令。

10.3.1. 实现AOF文件重写

AOF文件重写不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能通过读取服务器当前数据库状态实现。

重写过程:

def aof_rewrite(new_aof_file_name):
	# 创建新的AOF文件
	f = create_file(new_aof_file_name);
	# 遍历数据库
	for db in redisServer.db:
		# 忽略空数据库
		if db.is_empty() : continue;
		# 写入SELECT命令,指定数据库号码
		f.write_command("SELECT"+db.id);
		# 遍历数据库中所有键
		for key in db:
			# 忽略已经过期的键
			if key.is_expired():continue;
			# 根据键的类型对键进行重写
			if key.type == String:
				rewrite_string(key);
			else if  key.type == List:
				rewrite_list(key);
			else if  key.type == Hash:
				rewrite_hash(key);
			else if  key.type == Set:
				rewrite_set(key);
			else if  key.type == SortedSet:
				rewrite_sorted_set(key);

			# 如果键带有过期时间,那么过期时间也要被重写
			if key.have_expire_time():
				rewrite_expire_time(key);
		# 写入完毕,关闭文件
		f.close();

def rewrite_string(key):
	# 使用GET命令获取字符串键的值
	value = GET(key);
	# 使用SET命令重写字符串键
	f.write_command(SET, key, value);
def rewrite_list(key):
	# 使用LRANGE命令获取列表键的所有元素
	item1, item2,..., itemN = LANGE(key, 0, -1);
	
	# 使用RPUSH命令重写列表键
	f.write_command(RPUSH, key, item1, item2,..., itemN);

def rewrite_hash(key):
	# 使用HGETALL命令获取哈希键包含的所有键值对
	field1, value1, field2, value2,..., fieldN, valueN = HGETALL(key);
	
	# 使用HMSET命令重写哈希键键
	f.write_command(HMSET, key, field1, value1, field2, value2,..., fieldN, valueN);
def rewrite_set(key):
	# 使用SMEMBERS命令获取集合键的所有元素
	elem1, elem2,..., elemN = SMEMBERS(key);
	
	# 使用SADD命令重写集合键
	f.write_command(SADD, key, elem1, elem2,..., elemN);
def rewrite_sorted_set(key):
	# 使用ZRANGE命令获取有序集合键的所有元素
	member1, score1,member2, score2,...,memberN, scoreN  = ZRANGE(key, 0, -1, "WITHSCORES");
	
	# 使用ZADD命令重写有序集合键
	f.write_command(ZADD, key, member1, score1,member2, score2,...,memberN, scoreN);

def rewrite_expire_time(key):
	# 获取毫秒精度的键过期时间戳
	timestamp = get_expire_time_in_unixstamp(key);
	
	# 使用PEXPIREAT命令重写键的过期时间
	f.write_command(PEXPIREAT, key, timestamp);       

因为aof_rewrite函数生成的新AOF文件只包含还原当前数据库状态所必须的命令,所以新AOF文件不会包含浪费任何硬盘空间。

在这里插入图片描述

注意:实际中,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希键、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值(目前为64),那么重写程序将使用多条命令来记录键的值,而不适单单使用一条命令。

10.3.2. AOF 后台重写

AOF执行重写需要进行大量的写入操作,所以调用这个函数的线程会被长时间阻塞,因为Redis服务器使用单个线程来处理命令请求,所以如果服务器来调用aof_write函数的话,那么重写期间,服务器就无法处理客户端发来的命令请求。

所以,Redis将AOF重写放入到子进程里执行,目的两个:

  • 子进程进行AOF重写时,服务器进程(父进程)能继续执行命令请求
  • 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以避免使用锁的情况下,保证数据的安全性

但是问题又来了,子进程进行文件重写时,服务器可以继续处理命令,而新的命令可能会对现有的数据库状态进行修改,这就会导致服务器当前数据库装态和重写后的AOF文件所保存的数据流状态不一样。

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

在这里插入图片描述

现在,子进程执行AOF重写期间,服务器进程需要执行三个工作:

  1. 执行客户端发来的命令
  2. 将执行后的写命令追加到AOF缓冲区
  3. 将执行后的写命令追加到AOF重写缓冲区

这样就能保证:

  • AOF缓冲区的内容会被定期被写入和同步到AOF文件,对现有AOF文件的处理工作会如常进行
  • 从创建子进程开始,服务器执行的所有写命令都会呗记录到AOF重写缓冲区中

当子进程完成重写工作后,会向父进程发送一个信号,父进程在接到信号后,会调用一个信号处理函数,并执行以下工作:

  1. 将AOF重写缓冲区的所有内容写入到新AOF文件中,这是AOF文件所保存的数据库状态江河服务器当前的数据库状态一致
  2. 将新的AOF文件进行改名,原子地覆盖现有的AOF文件,完成新旧两个AOF文件的替换

在重写过程中,只用调用信号处理函数会阻塞服务器进程,其余时候不会阻塞父进程。

11. 事件

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

  • 文件事件

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

  • 时间事件

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

11.1. 文件事件

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

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

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

11.1.1. 文件事件处理器的构成

文件事件处理器 由四个部分组成:套接字、I/O多路复用程序、文件事件分派器以及事件处理器, I/O多路复用程序总是会将所有产生事件的套接字放到一个队列里面,然后通过这个队列以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后,I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。

在这里插入图片描述

1. 套接字

当一个套接字准备好执行连接应答、写入、读取、关闭等操作时,会产生一个文件事件。

因一个服务器通常会连接多个套接字,所以多个文件事件可能会并发出现

2. I/O多路复用程序

通过包装selectepollevportkqueue这些I/O多路复用函数实现。

负责监听多个套接字,并向文件事件分派器传送产生了事件的套接字。

I/O多路复用程序将昌盛事件的套接字放到一个队列中,通过该队列,以有序、同步、每次一个的方式向文件事件分派器传送套接字

上一个传送的套接字事件处理完毕后(关联的事件处理器执行完毕后),I/O多路复用程序才会向文件事件分派器传送下一个套接字 。

3. 文件事件分派器

接收I/O多路复用程序传来的套接字,根据套接字产生的时间类型,调用相应事件处理器。

4. 事件处理器

处理器是一个函数,定义某个时间发生时,服务器应执行的动作。

11.1.2. 事件类型

I/O多路复用程序可以监听多个套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件,这两类事件和套接字操作之间的对应关系如下:

  • 当套接字可读时(客户端对套接字执行write操作或者close操作),或者有新的可应答套接字出现时(客户端对服务器的监听套接字执行connect操作),套接字产生AE_READABLE事件。
  • 当套接字可写时(客户端对套接字执行read操作),套接字产生AE_WRITABLE事件。

注:Redis服务器优先处理同一个套接字的AE_READABLE事件,即,服务器先读套接字,后写套接字

11.1.3. 文件事件的处理器

文件事件有多个处理器,这些事件处理器 用于实现不同的网络通信需求。

  1. 连接应答处理器

    对连接服务器的监听套接字的客户端进行应答

    Redis初始化时,将监听套接字的AE_READABLE事件与该处理器关联

    客户端连接服务器时,产生AR_READABLE事件

    在这里插入图片描述

  2. 命令请求处理器

    接收客户端传来的命令请求

    客户端成功连接服务器后,Redis将该客户端套接字的AE_READABLE事件与该处理器关联

    客户端向服务器发送命令请求时,产生AE_READABLE事件,引发命令请求处理器执行,执行相应的套接字读入操作

    客户端连接服务器过程中,该客户端套接字的AE_READABLE事件始终与该处理器关联

    在这里插入图片描述

  3. 命令回复处理器

    向客户端返回命令执行结果

    服务器有命令执行结果要传送给客户端时,将客户端套接字的AE_WRITABLE事件始终与该处理器相关

    客户端准备好接收命令执行结果时,产生AE_WRITABLE事件

    命令执行结果传送结束后,关联解除

    在这里插入图片描述

  4. 复制处理器

    处理主服务器和从服务器的复制操作

11.1.4. 一次完整的客户端与服务器连接时间示例

在这里插入图片描述

11.2. 时间事件

两类时间事件:

  • 定时事件:让一段程序在指定时间执行一次
  • 周期性事件:让一段程序每隔指定时间执行一次

时间事件的是三个属性:

  • id:服务器为时间事件创建的全局唯一ID,从小到大递增,新比旧大
  • when:毫秒精度的UNIX时间戳,记录时间事件到达时间
  • timeProc:时间事件处理器,一个函数。时间事件到达时,服务器调用相应的服务器处理事件

时间事件类型取决于时间事件处理器的返回值:

  • 如果事件处理器返回ae.h/AE_NOMORE,定时事件:该事件达到一次之后就会被删除,之后不会到达
  • 返回一个非AE_NOMORE的整数值,周期性事件

11.2.1. 实现

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

这个无序指的是:不按when属性的大小排序,而不是不按ID排序,当时间事件处理器运行时,必须遍历链表中的所有时间事件,这样才能保证所有已到达的时间事件都会被执行。

无序链表不影响时间事件处理器的性能

正常模式下的Redis服务器只使用serverCron一个时间事件,而在benchmark模式下,服务器也只有两个时间事件。在这种情况下,服务器几乎是将无序链表退化成指针来使用,所以无序链表保存时间事件,不会影响事件执行的性能。

11.3. 事件的调度和执行

服务器中同时存在文件事件和时间事件,所以服务器必须对这两种事件进行调度,决定在什么时间处理什么事件,以及需要花多少时间。

事件z的调度和执行由ae.c/aeProcessEvents函数执行,伪代码如下:

def aeProcessEvents():
	# 获取到达时间离当前时间最接近的时间事件
	time_event = aeSearchNearesTimer();
	# 计算最接近的时间事件距离到达还有多少毫秒
	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();

注:processFileEvents这个函数虚构的,处理文件事件的代码直接写在aeProcessEvents函数里。

Redis服务器在事件处理角度的运行流程:

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/jun8148/article/details/82915676
今日推荐