Redis持久化
Redis是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将Redis中的数据以某种形式(数据或命令)从内存保存到硬盘,当下次Redis重启时,利用持久化文件实现数据恢复。
Redis持久化分为RDB持久化和AOF持久化:前者将当前数据保存到硬盘,后者则是将每次执行的写命令保存到硬盘(类似于MySQL的binlog)。
RDB
RDB持久化是将当前进程中的数据生成快照保存到硬盘,保存的文件后缀是rdb,当Redis重新启动时,可以读取快照文件恢复数据。RDB持久化分为手动触发和自动触发两种方式:
手动触发
手动触发可以使用save
命令和bgsave
命令,都可以生成rdb文件。它们的区别在于save
命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在Redis服务器阻塞期间,服务器不能处理任何命令请求;而bgsave
命令会创建一个子进程,由子进程来负责创建RDB文件,父进程(即Redis主进程)则继续处理请求,整个过程中只有fork
子进程时会阻塞服务器。
自动触发
自动触发最常见的情况是在配置文件中通过save m n
,指定当m秒内发生n次变化时,会触发bgsave
。例如默认配置文件中有以下三行:1
2
3save 900 1
save 300 10
save 60 10000
只要上面三行任意一条满足时,就会执行bgsave
。除此之外,在主从复制的场景下,如果从节点执行全量复制操作,则主节点会执行bgsave
命令,并将rdb文件发送给从节点。执行shutdown
命令时,也会自动执行rdb持久化。
启动时加载
RDB文件的载入工作是在服务器启动时自动执行的,并没有专门的命令。但是由于AOF的优先级更高,因此当AOF开启时,Redis会优先载入AOF文件来恢复数据;只有当AOF关闭时,才会在Redis服务器启动时检测RDB文件,并自动载入。服务器载入RDB文件期间处于阻塞状态,直到载入完成为止。
AOF
RDB持久化是将进程数据写入文件,而AOF持久化则是将Redis执行的每次写命令记录到单独的日志文件中(有点像MySQL的binlog),当Redis重启时再次执行AOF文件中的命令来恢复数据。与RDB相比,AOF的实时性更好,因此已成为主流的持久化方案。
三种策略
为了提高文件写入效率,在现代操作系统中,当用户将数据写入文件时(write
命令),操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但如果系统崩溃,内存缓冲区中的数据将会丢失。因此可以设置同步选项,强制操作系统什么时候将缓冲区中的数据写入到硬盘中(fsync
命令),Redis提供了以下三种同步策略:
- always:每个写命令都同步
- everysec:每秒同步一次
- no:让操作系统来决定何时同步
always会严重降低服务器的性能,而no的不可控性太强,因此Redis使用everysec作为默认配置,但在系统崩溃时可能会丢失一秒的数据。
文件重写
随着Redis服务器执行的写命令越来越多,AOF文件也会越来越大,过大的AOF文件不仅会影响服务器的正常运行,也会导致数据恢复需要的时间过长。文件重写是指定期重写AOF文件,减小AOF文件的体积。需要注意的是,AOF重写是把Redis进程内的数据转化为写命令,同步到新的AOF文件,而不会对旧的AOF文件进行任何读取、写入操作。
文件重写主要是针对以下一些语句:
- 过期的数据(如
expire
),可以不用再写入文件。 - 多次
INCR
命令可以合并为一个SET
命令。 - 无效的命令不再写入文件,比如有些数据被删除了。
手动触发
可以直接调用bgrewriteaof
命令重写文件,该命令的执行与bgsave
有些类似,都是fork
子进程进行具体的工作,且都只有在fork
时阻塞。
自动触发
默认配置是当AOF文件大小是上次重写后大小的一倍(auto-aof-rewrite-min-size
)且文件大于64M时触发(auto-aof-rewrite-percentage
)。
具体流程
- 父进程执行
fork
操作创建子进程,这个过程中父进程是阻塞的。 - 子进程创建后,Redis的所有写命令依然写入AOF缓冲区,并根据设置策略同步到硬盘,保证原有AOF机制的正确。
- 由于
fork
操作使用写时复制技术,子进程只能共享fork
操作时的内存数据。由于父进程依然在响应命令,因此Redis使用AOF重写缓冲区(图中的aof_rewrite_buf)保存这部分数据,防止新AOF文件生成期间丢失这部分数据。也就是说,bgrewriteaof执行期间,Redis的写命令同时追加到aof_buf和aof_rewirte_buf两个缓冲区。 - 子进程根据内存快照,按照命令合并规则写入到新的AOF文件。
- 子进程写完新的AOF文件后,向父进程发信号,父进程把AOF重写缓冲区的数据写入到新的AOF文件,这样就保证了新AOF文件所保存的数据库状态和服务器当前状态一致。
- 使用新的AOF文件替换老文件,完成AOF重写。
RDB与AOF对比
- RDB持久化:RDB文件紧凑,体积小,恢复速度比AOF快很多,但数据的实时性较低。
- AOF持久化:实时性较高,但是文件大,并且恢复速度较慢,且对性能有一定影响。
常见问题
fork阻塞:CPU的阻塞
在Redis中,无论是RDB持久化的bgsave
,还是AOF重写的bgrewriteaof
,都需要fork
出子进程来进行操作,而在操作系统fork
的实现中,基本都采用了写时复制技术,即在父/子进程试图修改数据空间之前,父子进程实际上共享数据空间,但是当父/子进程的任何一个试图修改数据空间时,操作系统会为修改的那一部分(内存的一页)制作一个副本。
也就是说,虽然fork
时子进程不会复制父进程的数据空间,但是会复制内存页表,如果Redis内存过大,会导致fork操作时复制内存页表耗时过多,而Redis主进程在进行fork
时是完全阻塞的,意味着无法响应客户端的请求,造成请求延迟过大。
为了防止该问题的发生,我们需要控制Redis单机内存的大小,并且适当放宽AOF重写的触发条件,尽量在写入较少的时间段完成重写。
AOF追加阻塞:硬盘的阻塞
AOF持久化过程中,通过fsync
命令每秒一次将缓冲区的数据写入磁盘中,但在硬盘负载过高时,fsync
操作可能会超过1s,当继续向缓冲区内写入数据时,磁盘负载会越来越大,如果此时Redis进程异常退出,丢失的数据也有可能远超1s。
为此,Redis的处理策略是这样的:主线程每次进行AOF会对比距离上次fsync
成功的时间,如果距上次不到2s,主线程直接返回;如果超过2s,则主线程阻塞直到fsync
同步完成。因此,如果系统硬盘负载过大导致fsync
速度太慢,会导致Redis主线程的阻塞。这里还要注意的是,如果使用everysec策略,AOF最多可能丢失2s的数据,而不是1s。
参考资料
- 《Redis开发与运维》
- 深入学习Redis(2):持久化