欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Redis 持久化

程序员文章站 2024-03-21 11:13:34
...

1. 持久化的意义

        Redis 是内存数据库,其将自己的数据存储在内存中,如果 Redis 发生宕机,且没有进行持久化的,那么Redis 重启后将没有之前的数据。而通过持久化,Redis可以在重启后,快速找回之前的数据,防止大量请求打入数据库。

       Redis持久化的方式有两种:RDB持久化、AOF持久化。

2. RDB 持久化

       RDB 持久化便是生成一个 RDB 文件,该文件是一个经过压缩的二进制文件。

Redis 持久化

2.1 RDB 文件的创建与载入

       Redis 有两个命令用于生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE

       SAVE`` 命令会阻塞Redis 服务器进程,直到RDB `文件创建完毕,期间不能处理任何客户端的命令。

       BGSAVE 命令则会创建一个子进程,由子进程负责创建 RDB文件,服务器进程依然可以继续处理命令请求。

       RDB文件的载入工作是在服务器启动时自动执行的,所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件

       因为AOF文件的更新频率通常比RDB文件的更新频率高,所以:

  • 如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态
  • 只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。

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

2.2 自动间隔性保存

       Redis允许用户通过设置服务器配置的 save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。

       用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。

Redis 持久化

       服务器程序会根据 save 选项所设置的保存条件,设置服务器状态 redisServer 结构的 saveparams 属性:

struct redisServer{
    
    // ...
    
    // 记录了保存条件的数组
    struct saveparam *saveparam;
    
    // ...
};
struct saveparam{
    
    // 秒数
    time_t seconds;
    
    // 修改数
    int changes;
}

       如果 save 选项的值同上,那么服务器状态中的 saveparams 数组如下图所示:

Redis 持久化

       服务器状态还维持了一个 dirty 计数器,以及一个lastsave属性:

  • dirty 计数器记录了距离上一次成功执行 SAVE 命令或者BGSAVE 命令之后,服务器对数据库状态(所有的数据)进行了多少次修改。
  • lastsave 属性记录了上一次成功执行 SAVE 命令或 BGSAVE 命令的时间。
struct redisServer{
    
    // ...
    
    // 修改计数器
    long long dirth;
    
    // 上一次执行保存的时间
    time_t lastsave;
}

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

def serverCron () :

	# ...
	
	# 遍历所有保存条件
	for saveparam in server.saveparams :
	
		# 计算距离上次执行保存操作有多少秒
		save_interval = unixtime_now() - server.lastsave
		
		# 如果数据库状态的修改次数超过条件所设置的次数
		# 并且距离上次保存的时间超过条件所设置的时间
		# 那么执行保存操作
		if server.dirty >= saveparam.changes and save_interval > saveparam.seconds :

			BGSAVE ()
	# ...

3. AOF 持久化

       除了RDB持久化功能之外,Redis 还提供了AOF ( Append Only File)持久化功能。

       RDB持久化通过保存数据库中的所有键值对,生成当前时刻的快照文件来记录数据库状态不同

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

Redis 持久化

3.1 AOF 持久化的实现

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

3.1.1 命令追加

       服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的 aof_buf 缓冲区的末尾。(注意,这里并没有直接写入到AOF文件中)

       每当有新的写入命令时,都会将命令先写入到aof_buf缓冲区内。

3.1.2 AOF 文件的写入和同步

       服务器每次结束一个文件事件循环之前,都可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以每次结束一个事件循环之前, 它都会调用 flushAppend0n1yFile函数,考虑是否需要将aof_ buf缓冲区中的内容写人和保存到AOF文件里面,这个过程可以用以下伪代码表示:

def eventLoop():

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

       flushAppendOblyFile 函数的是否进行写入AOF文件则是由服务器配置的 appendfsync 选项的值来决定。

Redis 持久化
       即使调用flushAppendOblyFile()函数,也不意味aof_buf缓冲区中的内容一定被写入到文件中,因为Redis和磁盘之间还有一层OS操作系统通常会将写入数据暂时保存在一个内存缓冲区里面,当缓冲区的空间被填满,或者超过了指定的时间后,才真正将缓冲区中的数据写入到磁盘里面

       这样便可能导致数据被操作系统写入到了内存缓冲区,但是OS还未来的及落地到磁盘,造成数据丢失。

       always则代表每执行一条写命令就落地到磁盘,但是Redis的效率会大幅降低。

       everysec则代表每隔一秒就落地到磁盘,该情况通常只会丢失掉一秒的数据。

       no则是由操作系统决定何时落地到磁盘,效率最高,如果宕机,则可能会丢失掉上次的同步到宕机时间内的数据。

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

Redis 持久化

3.3 AOF 重写

       随着服务器运行时间的流逝,AOF文件中的内容会越来越多,文件的体积也会越来越大,如果不加以控制的话,体积过大的AOF文件很可能对Redis服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。

       为了解决AOF文件体积膨胀的问题,Redis 提供了AOF文件重写( rewrite)功能。

       通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多。

3.3.1 AOF 重写的实现

       新的AOF文件并不是基于之前的AOF文件进行分析和重写的,因为这样的效率并没有直接扫描Redis中的键值对的速度快。

       首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前这个键值对的多条命令,这就是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)
			elif key.type == List:
				rewrite list (key)
			elif key.type == Hash:
				rewrite hash (key)
			elif key.type == Set:
				rewrite_set (key)
			elif 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命令获取列表键包含的所有元素
	iteml,item2, .... itemN = LRANGE (key, 0, -1)
        
	# 使用RPUSH命令重写列表键
	f.write_command (RPUSH, key, iteml, item2, .... itemN)
        
def rewrite_hash (key) :

	# 使用HGETALL命令获取哈希键包含的所有键值对
	fieldl, valuel, 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,scorel, member2 ,score2,...,memberN, scoreN 
					= ZRANGE (key, 0,-1,"WITHSCORES")
        
	# 使用ZADD命令重写有序集合键
	f. write_ command (ZADD, key, scorel, member1, score2, member2,scoreN,memberN)
        
def rewrite_expire_time (key) :
	# 获取毫秒精度的键过期时间戳
	timestamp = get_ expire_ time_ in_ unixstamp (key)
        
	# 使用PEXPIREAT命令重写键的过期时间
	f.write_command (PEXPIREAT, key, timestamp)

3.3.2 AOF 后台重写

       AOF重写为了避免造成阻塞,其是通过一个子进程来完成的。

       同时,为了防止AOF子进程重写的过程中,客户端又有新的写命令,造成数据不一致的情况。Redis服务器设置了一个AOF重写缓冲区,该缓冲区在服务器创建子进程后开始使用。当Redis服务器执行完一个写命令之后, 它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区。

Redis 持久化

       AOF重写完成后,子进程会给父进程发送一个信号,父进程便会执行如下操作:

  1. AOF重写缓冲区中所有内容写入到新AOF文件中,此时文件所保存的内容便和服务器数据一致了。
  2. 对新AOF文件进行改名,原子地覆盖现有AOF文件,完成新旧两个文件的替换。

4. 两种持久化方式的优点和缺点

4.1 RDB 持久化机制的优点和缺点

       优点

  • RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中Redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,以预定好的备份策略来定期备份Redis中的数据。

  • RDBRedis对外提供的读写服务,影响非常小,可以让Redis保持高性能,因为Redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可。

  • 相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复Redis进程,更加快速。

       缺点

  • 如果想要在Redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦Redis进程宕机,那么会丢失最近5分钟的数据。
  • RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。

4.2 AOF 持久化机制的优点和缺点

       优点

  • AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。
  • AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。
  • AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite的时候,通过子进程创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。
  • AOF日志文件的命令通过可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据。

       缺点

  • 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大。
  • AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的。

5. RDB 和 AOF 到底该如何选择

  • 不要仅仅使用RDB,因为那样会导致你丢失很多数据

  • 也不要仅仅使用AOF,因为那样有两个问题,第一,你通过AOF做冷备,没有RDB做冷备,来的恢复速度更快; 第二,RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug

  • 综合使用AOFRDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; 用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复。