Redis持久化RDB和AOF原理解析、使用和优缺点对比
前言
本文讲述 Redis 两种持久化方式 RDB 和 AOF 优缺点以及原理。
为何需要持久化?
Redis 是基于内存操作的,进程终止、服务器宕机后内存数据会丢失,但是在很多使用场景中我们希望数据不丢失,服务重启之后数据还能恢复到停机前的状态,特别是使用 Redis 做数据库的情况。
Redis 持久化就是在服务运行期间将数据写到磁盘上,进程重新启动的时候将磁盘上的数据加载到内存中,恢复到停机前的状态。Redis 有 RDB 和 AOF两种实现方式。
RDB、AOF 两种方式优劣
RDB 是意图在某一时刻保存一份完整的内存快照数据集到后缀为 .rdb的二进制文件中,文件中的内容是到那一刻为止内存中完整的数据状态,那一刻之后的操作跟它无关。
- 优点:因为是数据快照,所以生成的文件内容紧凑占用磁盘空间小,重启恢复到内存速度也较快,持久化的频率一般也会配置得比较低,并且执行过程交给子进程,对服务性能影响小。
- 缺点:因为是保存整个内存的数据,所以执行的整个过程会相对较长;因为间隔较长,会丢失较多的数据,在间隔期内服务进程终止的话上一次创建快照到终止那一刻对 Redis 数据的改动都会丢失。
AOF 则是在 .aof 文件中以追加写指令的方式实现的。
- 优点:因为追加写指令执行的频率高、间隔短,所以间隔期内进程停止丢失的数据较少,数据比较完整。
- 缺点:也是因为执行频率高,影响服务性能;写指令跟数据本身比占用空间较大,导致落到磁盘上的文件也很大,重启恢复的时间长。
RDB(Redis Database)
同/异步创建方式
RDB 有两种方式:通过 SAVE 指令同步执行,在明确创建过程中不对外提供服务或者明确会停机的时候使用;另一种是通执行 BGSAVE 指令,它会调用 fork() 函数创建子进程异步执行,默认、一般都是这种 。
fork() 和 copy-on-write
在触发保存 RDB的时候 Redis 会调用系统 fork() 函数创建出一个子进程将数据写到临时文件里,全部写完后再替换原来的 RDB 文件,文件最终都只有一份。子进程创建速度快,占用空间小,原因是用到了 Linux 系统写时拷贝技术(copy-on-write)。
写时拷贝技术(copy-on-write)如下图示:
【1】父进程调用系统 fork() 函数创建子进程,并复制一份父进程虚拟地址空间数据,其数据体积小,所以子进程创建速度快;
【2】虚拟地址保存的是物理空间的逻辑指针,物理空间则是真正保存数据的地方。父子进程指针一致,指向同一个物理内存实现了数据共享,子进程执行保存任务;
【3】主进程修改数据时,开辟新的物理内存写入数据,再修改指针变量的指向,父子进程互不干扰。
整个过程没有完整复制存储的数据,保存上 G 的数据也不必担心空间不足,虽然修改数据时占用了新的物理空间,但是占比很小。
配置
# 不保存 RDB
# save ""
# 下列是保存 RDB 的条件,可配置多个
# 经过 3600 秒,并且有 1 个以上 key 改变
save 3600 1
# 经过 300 秒 并且有 100 个以上 key 改变
save 300 100
# 经过 60 秒 并且有 10000 个以上 key 改变
save 60 10000
# 保存的文件名
dbfilename dump.rdb
# 保存的目录
dir /var/lib/redis/6379
AOF(Append Only File)
将所有写指令追加到一个缓冲区中,再按配置的策略由其他后台线程定期写到 AOF 文件中,宕机时缓冲区的数据会丢失。Redis 还会按配置定期对文件进行重写以减小文件的体积。
重写
重写同样运用了写时拷贝技术(copy-on-write)
在 4.0 版本之前重写就是对文件可以互相抵消的写指令进行简化,比如重复更新一个数据只需要保存最后一条指令,新增+删除则直接抵消。
Redis 执行 fork() ,子进程将 AOF 文件内容重写到临时文件。
对于新的写命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾,这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
最后 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。
在 4.0 之后版本 AOF 可以使用混合体,先生成 RDB 快照到 .aof 文件中,再追加写命令。
配置
# 开启 AOF
appendonly yes
# 文件名
appendfilename "appendonly.aof"
# 触发从缓存写到磁盘的策略
# 每条写指令触发
# appendfsync always
# 每秒触发
appendfsync everysec
# 让系统决定
# appendfsync no
# 用 RDB 混合方式,内容 "REDIS" 开头
aof-use-rdb-preamble yes
# Redis 记录每次重写时原文件大小,下次文件体积增长 100% 后触发重写
auto-aof-rewrite-percentage 100
# 文件大小达到 64 兆触发第一次重写
auto-aof-rewrite-min-size 64mb
管道与父子进程
- 你可以通过 $$ 和 $BASHPID 都代表当前 bash 进程 ID,你可以通过 echo 将它输出。
- 符号 | 可以用来创建管道,管道符左边命令的输出作为右边的命令的输入。如下图左边输出 123 管道右边 more 代表把接收的数据显示出来:
- 并且管道两边的指令会创建两个新的进程来执行。如下图:
【1】我们先用两种方式输出了当前解释进程ID;
【2】尝试将管道左边创建的进程 ID 作为右边进程参数输出出来,执行了两次可以发现 ID 都不一样,并且不等于外层进程 ID 9441,验证确实创建了新进程;
【3】用这种方式却发现输出的进程 ID 和外层进程一样的。是因为 $$ 优先级比管道符号 | 高,先将外层进程 ID 替换到了 echo 语句中,管道再创建了新进程执行这条已经完整的语句 echo 9441,所以不是用的新进程 ID。
- 常规情况下进程是数据隔离的。如下图:
【1】我们先创建了环境变量 num 赋值赋值为 1,并打印验证;
【2】/bin/bash 创建子进程,并打印进程 ID 对比进行验证;然后输出 num 显示空行代表没内容,验证进程间数据隔离。最后 exit 结束子进程。
- 父进程可以让子进程看到数据,父子进程修改数据互不影响,如下图:
【1】使用 export 指令,然后在子进程成功打印出了父进程的 num;
【2】在子进程修改数据并打印,验证子进程数据修改成功;
【3】exit 结束子进程,在父进程打印 num,验证父子进程修改数据互不影响。