Redis 持久化策略——RDB
Redis 之所以快,一个最重要的原因在于它是直接将数据存储在内存,并直接从内存中读取数据的,因此一个绝对不容忽视的问题便是,一旦 Redis 服务器宕机,内存中的数据将会完全丢失。
好在 Redis 官方为我们提供了两种持久化的机制,RDB 和 AOF,今天我们来聊一下 RDB。
什么是 RDB
RDB 是 Redis 的一种数据持久化到磁盘的策略,是一种以内存快照形式保存 Redis 数据的方式。所谓快照,就是把某一时刻的状态以文件的形式进行全量备份到磁盘,这个快照文件就称为 RDB 文件,其中 RDB 是 Redis DataBase 的缩写。
全量备份带来的思考
备份会不会阻塞主线程
我们知道 Redis 为所有客户端处理数据时使用的是单线程,这个模型就决定了使用者需要尽量避免进行会阻塞主线程的操作。那么 Redis 在生成 RDB 文件的时候,会不会阻塞主线程呢?
对此,Redis 提供了两个命令来生成 RDB,一个SAVE
,另一个是BGSAVE
。SAVE
命令会阻塞 Redis 的主线程,直到 RDB 文件创建完成为止,在此期间,Redis 不能处理客户端的任何请求。
与SAVE
直接阻塞主线程的做法不同,BGSAVE
命令会创建一个子进程,然后由子进程负责专门写入 RDB,主进程(父进程)继续处理命令请求,不会被阻塞。
注:主进程其实会阻塞在 fork()过程中,通常情况下该指令执行的速度比较快,对性能影响不大
RDB 文件实际是由rdb.c/rdbSave
函数进行创建的,SAVE
命令和BGSAVE
命令会以不同的方式调用这个函数,下面是两个命令的伪代码
对时刻备份还是对时段备份
现在我们已经知道如何对 Redis 某一时刻的状态进行全量备份了,需要重申的是,Redis 保存的是某一时刻的全量数据,而不是某一时间段内的全量数据。
为什么要执着于某一时刻的数据,一段时间内的数据不行吗?还真就不行!因为一个时刻的数据反映了系统的该时刻的状态。例如在t1
时刻,Redis 保存的数据状态为
t2
时刻,Redis 时刻的状态为
如果 Redis 保存的是一段时间内的全量数据,则在这一段时间内,数据有如下几种可能
只有第一条能完美表征 t1 时刻的系统状态,Redis 进行数据恢复时至少能恢复到t1
时刻的状态,t1
时刻之后的数据可通过其他方式(如之后会介绍到的持久化的另一种方式AOF
)进行补充,而其余 3 种数据对数据恢复没有任何实际意义。
备份过程中,数据能否修改
为了实现备份某一时刻数据的这个目的,如果是我们来设计 Redis,我们会怎么做呢?
一个自然的想法就是拷贝某一个时刻的 Redis 完整内存数据。这里自然就是子进程对主进程的内存进行全量拷贝了,然而这对于 Redis 服务几乎是灾难性的,考虑以下两个场景:
Redis 中存储了大量数据,
fork()
时拷贝内存数据会消耗大量时间和资源,会导致主进程一段时间的不可用Redis 占用了 10G 内存,而宿主机内存资源上限仅有 16G,此时无法对 Redis 的数据进行持久化
因此备份过程中不能进行内存数据的全量拷贝。
接下来我们需要关注的问题是,在对内存数据进行快照的过程中,数据还能被修改吗?这个问题至关重要,因为关系到 Redis 在快照过程中是否能正常处理写请求。
举个例子,我们在时刻t
为 Redis 进行快照,假设被内存数据量是 2GB,磁盘写入带宽是 0.2GB/S,不考虑其他因素的情况下,至少需要 10S(2/0.2=10)才能完全备份。如果在时刻t+5S
时,客户发送了一个修改目前未被写入内存的数据A
的写请求,被改成了A'
,如果此时A'
被写入磁盘,就会破坏快照的完整性,因为我们期望获得某一时刻的全量备份。
因此,快照过程中我们不希望有数据修改的操作。但这意味着在快照期间 Redis 无法处理处理的写操作,无疑会给义务服务带来巨大影响。而且我们知道 Redis 在快照期间是依然可以处理写请求的,接下来我们来分析一下 Redis 是如何解决我们刚刚提出的两个问题的。
Redis 写时复制(COW)
写时复制听起来非常的高端,吓退了不少技术爱好者,其原理其实非常非常简单,本质上就是“有写操作的时候复制一份”,是不是很简单?
注:写时复制不是 Redis 自身的特性,而是操作系统提供的技术手段。操作系统是一切技术的基础,所有技术的革新都必须建立在操作系统支持的基础上
Redis 主进程fork
生成的子进程可以共享主进程的所有内存数据,fork
并不会带来明显的性能开销,因为不会立刻对内存进行拷贝,它会将拷贝内存的动作推迟到真正需要的时候。
想象一下,如果主进程是读取内存数据,那么和BGSAVE
子进程并不冲突。如果主进程要修改 Redis 内存中某个数据(图中数据 C),那么操作系统内核会将被修改的内存数据复制一份(复制的是修改之前的数据),未被修改的内存数据依然被父子两个进程共享,被主进程修改的内存空间归属于主进程,被复制出来的原始数据归属于子进程。如此一来,主进程就可以在快照发生的过程中肆无忌惮地接受数据写入的请求,子进程也仍然能够对某一时刻的内容做快照。
注:写时复制是建立在短时间内写请求不多的假设之下,如果写请求的量非常巨大,那么内存复制的压力自然也不会小。
间隔自动备份
除了上文介绍的手动执行的SAVE
和BGSAVE
方法之外,Redis 还提供了配置文件的方式,可以每隔一定时间自动执行一次BGSAVE
方法。
例如,我们可以在 Redis 配置文件中设置如下参数(如果没有主动设置save
选项,则以下配置即为默认配置)
那么只要满足以下 3 个条件之一,BGSAVE
命令就会被执行
服务器在 900 秒内,对数据进行了至少 1 次的修改
服务器在 300 秒内,对数据进行了至少 10 次修改
服务器在 60 秒内,对数据进行了至少 10000 次修改
举个例子,以下是 Redis 服务器在 300 秒内,对数据进行了至少 10 次修改之后,服务器自动进行BGSAVE
命令时打印的日志
自动保存的原理
savaparams 属性
Redis 会根据配置文件中设置的保存条件(或者未配置时的默认配置),设置服务器状态的redisServer
的saveparams
属性
saveparams
是一个数组,数组中每个对象都是saveparam
结构,saveparam
结构如下所示,每个字段分别表征save
选项的参数
以默认配置为例,Redis 中saveparams
存储的数据结构将会如下所示
dirty 计数器和 lastsave 属性
除了saveparams
参数之外,redisServer
还有dirty
和lastsave
属性
dirty
属性保存距离上次成功执行 RDB 快照之后,Redis 对数据进行了多少次修改操作(包括写入、更新、删除)lastsave
属性记录了 Redis 上一次成功执行 RDB 快照的时间,是一个 UNIX 时间戳
Redis 每进行一次写命令都会对dirty
计数器进行更新,批量操作按多次进行计数,如
dirty
计数器将会增加 3
如上图所示,dirty
计数器的值为 101,标识 Redis 自上次成功进行 RDB 快照之后,对数据库一共进行了 101 次修改操作;lastsave
属性记录了上次成功进行 RDB 快照的时间 1638023962(2021-11-27 22:39:22)
周期性检查保存条件
serverCron
函数默认每隔 100 毫秒就会执行一次,该函数的其中一个作用就是检查save
命令设置的保存条件是否被满足,是则执行BGSAVE
命令。伪代码如下
再举个例子,假设 Redis 的当前状态如下图
那么当时间来到 1638024263(1638023962 之后的第 301 秒),由于满足了saveparams
数组的第 2 个保存条件——300S 之内至少进行 10 次修改,Redis 将会执行一次BGSAVE
操作。
假设BGSAVE
执行 4S 之后完成,则此时 Redis 的状态将会更新为
版权声明: 本文为 InfoQ 作者【蝉沐风】的原创文章。
原文链接:【http://xie.infoq.cn/article/aee7410c3c7ffb1fa598383a9】。文章转载请联系作者。
评论