Redis持久化
RDB
Redis默认机制–将内存中的数据集快照以二进制的方式写入磁盘(RDB文件是一个经过压缩的二进制文件)。有两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE。
触发方式
save(主动触发)
- 由主进程完成,会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间。
- 所以当SAVE命令正在执行时,客户端发送的所有命令请求都会被拒绝。只有在服务器执行完SAVE命令、重新开始接受命令请求之后,客户端发送的命令才会被处理。
- RDB文件的载入工作是(主进程)在服务器启动时(检测到RDB文件存在)自动执行的(开启AOF后,优先加载AOF文件)。
- 如果内存数据量大的话会造成长时间的阻塞,线上环境一般禁止使用

bgsave(自动触发)
执行bgsave命令时Redis主进程会fork一个子进程来完成RDB的过程,完成后自动结束(操作系统的多进程Copy On Write机制,简称COW)。所以Redis主进程阻塞时间只有fork阶段的那一下,之后主进程继续处理命令请求。相对于save过程,阻塞时间很短。

触发条件(save配置)
// 默认配置;也可以自定义配置触发条件
save 900 1 #900秒内如果超过1个key被修改,则发起快照保存
save 300 10 #300秒内容如超过10个key被修改,则发起快照保存
save 60 10000 #60秒内容如超过10000个key被修改,则发起快照保存
服务器程序将根据save选项使用saveparams属性保存起来,放在服务器状态redisServer结构中。当任意条件触发后[服务器周期性操作函数serverCron(默认每隔100毫秒就会执行一次)],自动开始执行bgsave功能。
- 根据我们的save m n配置规则触发;
- 从节点全量复制时,主节点发送rdb文件给从节点完成复制操作,主节点会触发bgsave;
- 执行debug reload时;
- 执行shutdown时,如果没有开启aof,也会触发
- 主从同步(slave和master建立同步机制)
过程
Redis 使用操作系统的多进程 cow(Copy On Write) 机制来实现RDB快照持久化
- RDB触发时,Redis父进程会自动执行bgsave命令,且会先检查是否有子进程在执行RDB/AOF(save、bgsave/bgrewriteaof)持久化任务
- 如果有,则bgsave命令直接返回;如果没有,Redis父进程执行fork操作创建子进程,这个过程中Redis父进程是阻塞的,不执行来自客户端的任何命令
- 父进程fork完成后,bgsave命令返回”Background saving started”信息,并不再阻塞父进程,可以响应客户端的命令。(子进程进行RDB的过程中父进程的读写不受影响,但写操作不会同步到父进程的主内存中,而是会写到一个临时的内存区域)
- 子进程会根据Redis父进程的内存快照生成临时的快照文件(RDB文件:dump.rdb),完成后,会使用此临时快照文件对原来的RDB文件进行原子替换,然后删除原来的RDB文件(所以绝大部分情况下,Redis都只有一个RDB文件)。
- 子进程完成RDB持久化后,会发消息给父进程,通知RDB持久化完成(主进程将内存中临时缓冲区的增量写数据同步到主内存)
注:
a. 主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题
b. Fork函数的作用:复制一个与当前进程一样的进程,该子进程与父进程享有相同的地址空间,新进程的所有数据数值都和原进程一致。是一个全新的进程,并作为原进程的子进程
启动时RDB文件载入

save和bgsave区别
SAVE命令和BGSAVE命令 会以不同的方式调用这个函数,伪代码如下:
def SAVE ():
创建RDB文件
rdbSave()
def BGSAVE (:
创建子进程
pid - fork()
if pid ==0:
+子进程负责创建RDB文件
rdbSave ()
*完成之后向父进程发送信号 signal_parent ()
elif pid > 0:
*父进程继续处理命令请求,并通过轮询等待子进程的信号
handle_request_and_wait_signal()
else:
优点
- RDB文件小,非常适合定时备份,用于灾难恢复
- RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。因为存储的是内存中的数据;而AOF文件中存储的是一条条命令,需要重演命令。 AOF瘦身? 100条同样写 -> 一条写
缺点
- RDB无法做到实时持久化,若在两次bgsave间宕机,则会丢失区间(分钟级)的增量数据,不适用于实时性要求较高的场景
- RDB的cow机制中,fork子进程属于重量级操作,并且会阻塞redis主进程
- 存在老版本的Redis不兼容新版本RDB格式文件的问题
增量快照(RDB)
- 背景:如果一直使用全量同步,一方面时间的推移,磁盘存储的快照文件会越来越多。另一方面如果频繁的进行全量同步,则需要主线线程频繁的fork出bgsvae线程,这样对Redis的性能是会产生影响的,并且也需要持续的对磁盘进行写操作。
- 思路:做了一次全量快照后,为了避免后续每次全量快照的开销,后续的快照只对修改的数据进行快照记录。
- 实现:在第一次做完全量快照后,T1 和 T2 时刻如果再做快照,我们只需要将被修改的数据写入快照文件就行。
- 关键:实现的前提,需要我们使用额外的元数据信息去记录哪些数据被修改了。这会占用系统内存,带来额外的空间开销问题(这对于内存资源宝贵的 Redis 来说,有些得不偿失,实际应用时,则要根据具体情况进行权衡)。如下图所示

RDB重点回顾
- RDB文件用于保存和还原Redis服务器所有数据库中的所有键值对数据。
- SAVE命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器。
- BGSAVE令由子进程执行保存操作,所以该命令不会阻塞服务器。
- 服务器状态中会保存所有用save选项设置的保存条件,当任意一个保存条件被满足时,服务器会自动执行BGSAVE命令。
- RDB文件是一个经过压缩的二进制文件,由多个部分组成。
- 对于不同类型的键值对, RDB文件会使用不同的方式来保存它们
AOF
append only file–将客户端对服务器的写命令以Redis协议追加保存到以后缀aof文件的末尾。在Redis服务器重启时,会重演aof文件的命令,以达到恢复数据的目的。

机制及过程
命令传播
Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中;
当一个Redis客户端需要执行命令时,它通过网络连接,将协议文本发送给Redis服务器。服务器在接到客户端的请求之后, 它会根据协议文本的内容,选择适当的命令函数,并将各个参数从字符串文本转换为Redis字符串对象(StringObject )。每当命令函数成功执行之后,命令参数都会被传播到AOF 程序。
命令追加
AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的 AOF 缓存中。
当命令被传播到 AOF 程序之后, 程序会根据命令以及命令的参数, 将命令从字符串对象转换回原来的协议文本。协议文本生成之后, 它会被追加到 redis.h/redisServer 结构的 aof_buf 末尾。redisServer 结构维持着 Redis 服务器的状态, aof_buf 域则保存着所有等待写入到 AOF 文件的协议文本(RESP)。
AOF文件的写入和保存
注:命令的写入,这是一个 IO 操作。Redis 为了提升写入效率,它不会将内容直接写入到磁盘中,而是将其放到一个内存缓存区(buffer)中,等到缓存区被填满(触发写入条件)时,才真正将缓存区中的内容写入到磁盘(AOF文件)里。
因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面(写入),所以在服务器每次结束一个事件循环之前,它都会调用flushAppendonlyFile函数,考虑是否需要将aof_buf缓冲区中的内容保存到AOF文件里面(保存)。
- Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。
- flushAppendonlyFile函数的行为由服务器配置的appendfsync选项的值来决定,各个不同值产生的行为


因为处于no模式下的flushAppendOnlyFile调用无须执行同步操作。所以,该模式下的AOF文件写入速度总是最快的,不过因为这种模式会在系统缓存中积累一段时间的写入数据。所以,该模式的单次同步时长通常是三种模式中时间最长的。 从平摊操作的角度来看,no模式和everysec模式的效率类似,当出现故障停机时,使用no模式的服务器将丢失上次同步AOF文件之后的所有写命令数据
写入和保存
*注:fsync 或 fdatasync 函数可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,确保写入数据的安全性
- SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 aof_buf 保存到磁盘(AOF文件)中。因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求。
- WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
AOF文件载入与数据还原
- 创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样;
- 从AOF文件中分析并读取出一条写命令;
- 使用伪客户端执行被读出的写命令;
- 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止

配置
AOF默认是关闭的,通过redis.conf配置文件进行开启
## 此选项为aof功能的开关,默认为“no”,可以通过“yes”来开启aof功能
## 只有在“yes”下,aof重写/文件同步等特性才会生效
appendonly yes
## 指定aof文件名称
appendfilename appendonly.aof
## 指定aof操作中文件同步策略,有三个合法值:always everysec no,默认为everysec
appendfsync everysec
## 在aof-rewrite期间,appendfsync是否暂缓文件同步,"no"表示“不暂缓”,“yes”表示“暂缓”,默认为“no”
no-appendfsync-on-rewrite no
## aof文件rewrite触发的最小文件尺寸(mb,gb),只有大于此aof文件大于此尺寸是才会触发rewrite,默认“64mb”,建议“512mb”
auto-aof-rewrite-min-size 64mb
## 相对于“上一次”rewrite,本次rewrite触发时aof文件应该增长的百分比
## 每一次rewrite之后,redis都会记录下此时“新aof”文件的大小(例如A)
## aof文件增长到A*(1 + p)之后,触发下一次rewrite,每一次aof记录的添加,都会检测当前aof文件的尺寸。
auto-aof-rewrite-percentage 100
AOF Rewrite(重写瘦身)
基于Copy On Write,Redis可以在 AOF体积变得过大时,自动地在后台(Fork子进程)进行 AOF进行重写。重写后的新 AOF文件包含了恢复当前数据集所需的最小命令集合。 AOF重写并不需要对原有的 AOF 文件进行任何写入、读取和分析, 它会全量遍历数据库内存中数据,然后逐个序列到AOF文件中。因此AOF rewrite能够正确反应当前内存数据的状态。

重写流程
- 判断当前是否存在正在执行 bgsave/bgrewriteaof的子进程,如果存在则bgrewriteaof命令直接返回;如果存在bgsave命令则等bgsave执行完成后再执行。
- 父进程执行fork操作创建子进程,fork过程中父进程是阻塞的(等同RDB中的bgsave),fork完毕后父进程才能继续响应其他客户端的请求。重写采用写时复制:
a. 父进程fork出一个子进程,并把父进程那一时刻的内存数据拷贝给子进程,子进程对着这份副本数据逐一写入到新aof文件。
b. 子进程重写期间,由于父进程还可响应客户端命令,它接受的写命令都同时追加到aof_buf和 aof_rewirte_buf 两个缓冲区中,防止新AOF文件生成期间丢失这部分数据。 - 子进程完成AOF重写后,向父进程发送完成信号。父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作(这期间父进程阻塞对外的请求)
a. 将AOF重写缓冲区中的所有内容写人到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
b. 对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换 - 这个信号处理函数执行完毕之后,父进程就可以继续像往常一样接受命令请求了。
流程简化如下:

Redis 不希望 AOF 重写造成服务器无法处理请求, 所以 Redis 决定将 AOF 重写程序放到(后台)子进程里执行, 这样处理的最大好处是:
5. 子进程进行 AOF 重写期间,主进程可以继续处理命令请求。
6. 子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性
重写兜底方案
增加重写缓冲区。由于子进程重写期间,父进程还可接受其他客户端的写请求,会导致重写后的AOF文件和当前服务器状态不一致。为了解决这种数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区,子进程重写完毕,父进程会把aof_rewrite_buf重写缓冲区的数据追加到新aof文件中,确保数据的一致(aof_buff保存的是aof缓冲的写命令,而aof_rewrite_buff缓冲的是rewrite期间的命令)。
AOF重写时主进程工作
- 处理命令请求;
- 将写命令追加到现有的 AOF 文件中;
- 将写命令追加到 AOF 重写缓存中。

以上三个操作可以保证现有的 AOF 功能会继续执行,即使在 AOF 重写期间发生停机,也不会有任何数据丢失,所有对数据库进行修改的命令都会被记录到 AOF 重写缓存中。
这样一来可以保证:
- AOF缓冲区的内容会定期被写人和同步到AOF文件,对现有AOF文件的处理工作会如常进行。
- 从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面
父进程为什么同时向 aof_buf 和 aof_rewrite_buf 两个缓冲区写入数据?
- aof_buff保存的是aof期间缓冲的写命令,而aof_rewrite_buff缓冲的是rewrite期间的命令;
- 在AOF重写日志期间发生宕机(即子进程重写过程中发生意外),新aof文件还没来得及替换旧的,故恢复数据时,用的还是旧的aof文件,即写入aof_buf 缓冲区的数据。
- 使用子进程也有一个问题需要解决: 因为子进程在进行 AOF 重写期间, 主进程还需要继续处理命令, 而新的命令可能对现有的数据进行修改, 这会让当前数据库的数据和重写后的 AOF 文件中的数据不一致。为了解决这个问题, Redis 增加了一个 AOF 重写缓存, 这个缓存在 fork 出子进程之后开始启用,Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 缓存文件之外,还会追加到这个缓存中;当内存中的数据被全部写入到新的AOF文件之后,收集的新的变更操作也将被一并追加到新的AOF文件中;然后将新AOF文件重命名为appendonly.aof,使用新AOF文件替换老文件(替换成功后会删除旧aof文件),此后所有的操作都将被写入新的AOF文件。
触发方式
手动
直接调用bgrewriteaof命令
自动
根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机:(aof_current_size > auto-aof-rewrite-min-size ) && (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage
- auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB。
- auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值。默认为 1
默认自动触发时机:当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。这里的“一倍”和“64M” 可以通过配置文件修改
AOF优点
AOF只是追加写日志文件,对服务器性能影响较小,速度比RDB要快,消耗的内存较少
AOF缺点
- AOF方式生成的日志文件太大,需要不断AOF重写,进行瘦身,也会频繁引起磁盘IO。
- 即使经过AOF重写瘦身,由于文件是文本文件,文件体积较大(相比于RDB的二进制文件)。
- AOF重演命令式的恢复数据,速度显然比RDB要慢
AOF和RDB对比
二者都消耗磁盘IO。Redis采取了“schedule”策略:无论是“人工干预”还是系统触发,快照和重写需要逐个被执行
AOF重点回顾
- AOF文件通过保存所有修改数据库的写命令请求来记录服务器的数据库状态
- AOF文件中的所有命令都以Redis命令请求协议的格式保存。
- 命令请求会先保存到aof_buff缓冲区里面,之后再定期写入并同步到AOF文件。
- appendfsync选项的不同值对AOF持久化功能的安全性以及Redis服务器的性能有很大的影响。
- 服务器只要载人并重新执行保存在AOF文件中的命令,就可以还原数据库本来的状态。
- AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
- AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读入、分析或者写入操作。
- 在执行BGREWRITEAOF命令时, Redis服务器会维护一个AOF重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完 成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF 文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新 的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作
RewriteAOF伪代码
def aof_rewrite(new_aof_file_name):
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_1ist (key)
elif key.type Hash:
rewrite_hash (key)
elif key.type == Set:
rewrite_set (key)
elif key.type -- SortedSet:
rewrite_sorted_set (key)
#如果键带有过期时间,那么过期时间也要被重写
1f 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, itemi, item2, ...., itemN)
def rewrite_hash (key):
#使用HGETALL命令获取哈希键包含的所有健值对
fieldı, valuel, field2, value2, .. fieldN, valueN - HGETALL (key)
使用HMSET命令重写哈希键
f.write_command (HMSET, key, fieldi, valuel, fieldz, value2, ...fieldN,valueN)
def rewrite_set (key)
#使用SMEMBERS命令获取集合键包舍的所有元素
elemı, elem2, ..., elemN - SMEMBERS (key)
#使用SADD命令重写集合键
f.write_command (SADD, key, eleml, elem2, ..., elemN)
def rewrite_sorted_set (key):
#使用ZRANGE命令获取有序集合键包舍的所有元素
memberi, 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)
混合场景持久化(Redis4.0)
开启混合持久化aof-use-rdb-preamble yes
内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。这样一来,快照不用很频繁的执行,然后 AOF 日志也不用记录所有的命令了,只记录两次快照中间的。通常这部分AOF日志很小,可以避免重写开销。
相当于:
- 大量数据使用粗粒度(时间上)的rdb快照方式,性能高,恢复时间快。
- 增量数据使用细粒度(时间上)的AOF日志方式,尽量保证数据的不丢失
在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升
其他方式持久化
Master使用AOF,Slave使用RDB快照。master需要首先确保数据完整性,它作为数据备份的第一选择;slave提供只读服务或仅作为备机,它的主要目的就是快速响应客户端read请求或灾备切换
6783

被折叠的 条评论
为什么被折叠?



