1. 概述
我们知道 Redis 是一个内存数据库,也就意味着如果我们的电脑异常重启或者服务器宕机的情况下,存储在 Redis 中的数据会丢失。
Redis 虽然是个内存数据库,但是 Redis 支持 RDB 和 AOF 两种持久化机制,将数据写往磁盘,可以有效地避免因进程退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即可实现数据恢复。
2. RDB
RDB(Redis DataBase) 持久化是把当前进程数据生成快照保存到硬盘的过程。什么是快照?你可以理解成把当前时刻的数据拍成一张照片保存下来。
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb
。
前面说了,RDB 是快照的方式来保存数据的,因此对于 RDB 来说,提供了两种触发机制, 手动触发和自动触发。
手动触发分别对应 save
和 bgsave
命令。
2.1 save
save
命令会阻塞当前 Redis 服务器,直到 RDB 过程完成为止。
因此,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。
2.2 bgsave
bgsave
命令执行时,Redis 进程执行 fork 操作创建子进程,RDB 持久化过程由子进程负责,完成后自动结束。阻塞只发生在 fork 阶段,一般时间很短。
显然 bgsave
命令是针对 save
阻塞问题做的优化。因此 Redis 内部所有的涉及 RDB 的操作都采用 bgsave
的方式。其具体流程如下:
-
执行
bgsave
命令,Redis 父进程判断当前是否存在正在执行的子进程,如 RDB/AOF 子进程,如果存在,bgsave 命令直接返回。 -
父进程执行 fork 操作创建子进程,fork 操作过程中父进程会阻塞,通过
info stats
命令查看latest_fork_usec
选项,可以获取最近一个 fork 操作的耗时,单位为微秒。 -
父进程 fork 完成后,
bgsave
命令返回 **Background saving started **信息并不再阻塞父进程,可以继续响应其他命令。 -
子进程创建 RDB 文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行 lastsave 命令可以获取最后一次生成 RDB 的时间,对应 info 统计的 rdb_last_save_time 选项。
127.0.0.1:6379> lastsave (integer) 1640574329
-
进程发送信号给父进程表示完成,父进程更新统计信息。
2.3 自动触发
除了执行命令手动触发之外,Redis 内部还存在自动触发 RDB 的持久化机制。自动触发是由我们的配置文件来完成的,在redis.conf
配置文件中,有如下配置:
-
save
save m n,指在 m 秒内,如果有 n 个键发生改变,则自动触发持久化(执行一次 bgsave 命令)。例如,save 60 1 则表明在 60 秒内,至少有一个键发生改变,就会触发 RDB 持久化。
-
stop-writes-on-bgsave-error
默认值为 yes。当启用了 RDB 且最后一次后台保存数据失败,Redis 是否停止接收数据。这会让用户意识到数据没有正确持久化到磁盘上,否则没有人会注意到灾难(disaster)发生了。如果Redis重启了,那么又可以重新开始接收数据了。
-
rdbcompression
默认值是 yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。
-
dbfilename
设置快照的文件名,默认是 dump.rdb。
-
dir
设置快照文件的存放路径,这个配置项一定是个目录,而不能是文件名,默认为
./
。
2.4 RDB 文件
RDB 生成的持久化文件默认为dump.rdb
,可在配置文件中设置,也可以通过以下命令查找:
find / -name dump.rdb
# linux 下查找
[root@localhost ~]# find / -name dump.rdb
/usr/local/redis/redis-6.2.6/src/dump.rdb
RDB 文件保存在 dir 配置指定的目录下,文件名通过 dbfilename 配置指定。
可以通过执行 config set dir {newDir}
和 config set dbfilename {newFileName}
运行期动态执行,当下次运行时 RDB 文件会保存到新目录。
Redis 默认采用 LZF 压缩算法对生成的 RDB 文件做压缩处理,压缩后的文件远远小于内存大小,默认开启,可以通过参数 config set rdbcompression {yes|no}
动态修改。
虽然压缩 RDB 会消耗 CPU,但可大幅降低文件的体积,方便保存到硬盘或通过网维示络发送给从节点,因此线上建议开启。
如果 Redis 加载损坏的 RDB 文件时拒绝启动,并打印如下日志:
# Short read or 0OM loading DB. Unrecoverable error,aborting now.
这时可以使用 Redis 提供的 redis-check-dump 工具检测 RDB 文件并获取对应的错误报告。
2.5 RDB 的优缺点
2.5.1 优点
- RDB 是一个紧凑压缩的二进制文件,代表 Redis 在某个时间点上的数据快照。非常适用于备份,全量复制等场景。 比如每隔几小时执行
bgsave
备份,并把 RDB 文件拷贝到远程机器或者文件系统中(如 hdfs),用于灾难恢复。 - 生成 RDB 文件的时候,Redis 主进程会 fork 一个子进程来处理所有保存工作,主进程不需要进行任何磁盘 IO操作。
- Redis 加载 RDB 恢复数据远远快于 AOF 的方式。
2.5.2 缺点
- RDB 方式数据没办法做到实时持久化/秒级持久化。因为
bgsave
每次运行都要执行 fork 操作创建子进程,属于重量级操作,频繁执行成本过高。 - RDB 文件使用特定二进制格式保存,Redis 版本演进过程中有多个格式的 RDB 版本,存在老版本 Redis 服务无法兼容新版 RDB 格式的问题。
针对 RDB 不适合实时持久化的问题,Redis 提供了 AOF 持久化方式来解决。
3. AOF
AOF(Append Only File)持久化,以独立日志的方式记录每次写命令,重启时再重新执行 AOF 文件中的命令达到恢复数据的目的。
AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。相比 RDB 的全量备份节省了很多时间。
开启 AOF 功能需要设置配置,默认不开启。AOF 文件名通过 appendfilename 配置设置,默认文件名是 appendonly.aof。保存路径同 RDB 持久化方式一致,通过 dir 配置指定。
# 开启 AOF
appendonly yes
# AOF 文件名
appendfilename "appendonly.aof"
# always 每收到写命令就立即强制写入磁盘,最慢的,但是保证完全的持久化,不推荐使用
# everysec 每秒强制写入磁盘一次,性能和持久化方面做了折中,推荐
appendfsync everysec
# 正在导出rdb快照的过程中,要不要停止同步aof
no-appendfsync-on-rewrite yes
# aof文件大小比起上次重写时的大小,增长率100%时,重写
auto-aof-rewrite-percentage 100
# aof文件,至少超过64M时,重写
auto-aof-rewrite-min-size 64mb
3.1 工作流程
AOF 的工作流程如下:
-
命令写入(append)
所有的写入命令会追加到 aof_buf(缓冲区)中。
-
文件同步(sync)
AOF 缓冲区根据对应的策略向硬盘做同步操作。
-
文件重写(rewrite)
随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
-
重启加载(load)
当 Redis 服务器重启时,可以加载 AOF 文件进行数据恢复。
3.2 命令写入
AOF 命令写入的内容直接是 RESP 文本协议格式。例如 set hello world 这条命令,在 AOF 缓冲区会追加如下文本:
* 3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
AOF 为什么直接采用文本协议格式?
文本协议具有很好的兼容性。开启 AOF 后,所有写入命令都包含追加操作,直接采用协议格式,避免了二次处理开销。文本协议具有可读性,方便直接修改和处理。
AOF 为什么把命令追加到 aof_buf 中?
Redis 使用单线程响应命令,如果每次写 AOF 文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。先写入缓冲区 aof_buf 中,还有另一个好处,Redis 可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。
3.3 文件同步
Redis 提供了多种 AOF 缓冲区同步文件策略,由参数 appendfsync
控制。主要有以下三种:
-
always
命令写入 aof_buf 后调用系统 fsync 操作同步到 AOF 文件,fsync 完成后线程返回命令 fsync 同步文件。
配置为 always 时,每次写入都要同步 AOF 文件,在一般的 SATA 硬盘上,Redis 只能支持大约几百 TPS 写入,显然跟 Redis 高性能特性背道而驰,不建议配置。
-
everysec
写人 aof_buf 后调用系统 write 操作,write 完成后线程返回。操作由专门线程每秒调用一次 fsync 命令。
配置为 everysec,是建议的同步策略,也是默认配置,做到兼顾性能和数据安全性。理论上只有在系统突然宕机的情况下丢失 1 秒的数据。
-
no
写入 aof_buf 后调用系统 write 操作,不对 AOF 文件做 fsync 同步,同步硬盘操作由操作系统负责,通常同步周期最长 30 秒。
配置为 no,由于操作系统每次同步 AOF 文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但数据安全性无法保证。
系统调用 write 和 fsync
write
操作会触发延迟写(delayed write)机制。Linux 在内核提供页缓冲区用来提高硬盘 IO 性能。write 操作在写入系统缓冲区后直接返回。同步硬盘操作依赖于系统调度机制,例如:缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。
fsync
针对单个文件操作(比如 AOF 文件),做强制硬盘同步,fsync 将阻塞直到写入硬盘完成后返回,保证了数据持久化。
3.4 文件重写
随着命令不断写入 AOF,文件会越来越大,为了解决这个问题,Redis 引入 AOF 重写机制压缩文件体积。AOF 文件重写是把 Redis 进程内的数据转化为写命令同步到新 AOF 文件的过程。
重写后的 AOF 文件为什么可以变小?原因如下:
- 进程内已经超时的数据不再写入文件。
- 旧的 AOF 文件含有无效命令,如 set a 111、set a 222 等。重写使用进程内数据直接生成,这样新的 AOF 文件只保留最终数据的写入命令。
- 多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush listc 可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢出,对于 list、set、hash、zset 等类型操作,以 64 个元素为界拆分为多条。
AOF 重写降低了文件占用空间,除此之外,另一个目的是:更小的 AOF 文件可以更快地被 Redis 加载。
AOF 重写过程可以手动触发和自动触发:
-
手动触发:直接调用
bgrewriteaof
命令。该命令会将内存中的数据以命令的方式保存到临时文件中,同时会 fork 出一条新进程来将文件重写。
-
自动触发:根据
auto-aof-rewrite-min-size
和auto-aof-rewrite-percentage
参数确定自动触发时机。auto-aof-rewrite-min-size
:表示运行 AOF 重写时文件最小体积,默认为 64 MB。auto-aof-rewrite-percentage
:代表当前 AOF 文件空间(aof_currentsize)和上一次重写后 AOF 文件空间(aof_base_size)的比值。
3.5 重启加载
AOF 和 RDB 文件都可以用于服务器重启时的数据恢复。redis 重启时加载 AOF 与 RDB 的顺序是怎么样的呢?
- 当 AOF 和 RDB 文件同时存在时,优先加载 AOF;
- 若关闭了 AOF,加载 RDB 文件;
- 加载 AOF/RDB 成功,redis 重启成功;
- AOF/RDB 存在错误,启动失败打印错误信息。
3.6 AOF 的优缺点
3.6.1 优点
- AOF 可以更好的保护数据不丢失,一般 AOF 会每隔 1 秒,通过一个后台线程执行一次 fsync 操作,最多丢失 1 秒钟的数据;
- AOF 日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损;
- AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写;
- AOF 日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用 flushall 命令清空了所有数据,只要这个时候后台 rewrite 还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条 flushall 命令给删了,然后再将该 AOF 文件放回去,就可以通过恢复机制,自动恢复所有数据。
3.6.2 缺点
- 对于同一份数据来说,AOF 日志文件通常比RDB数据快照文件更大。
- AOF 开启后,支持的写 QPS 会比 RDB 支持的写 QPS 低,因为 AOF 一般会配置成每秒 fsync 一次日志文件,当然,每秒一次 fsync,性能也还是很高的。
- 以前 AO F发生过 bug,就是通过 AOF 记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。
4. 如何选择
对于 RDB,它能够在指定的时间间隔对内存中的数据进行快照存储。
对于 AOF,他能记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以文本协议追加保存每次写的操作到文件末尾。Redis 还能够对 AOF 文件进行后台重写,使 AOF 文件体积不至于过大。
两种方式同时开启,在两种方式同时开启的情况下,Redis 启动的时候会优先加载 AOF 文件来恢复原始数据,因为在通常情况下 AOF 文件保存的数据集比 RDB 文件保存的数据集要完整,RDB 的数据会不实时。
那么只使用 AOF 呢?建议不要,因为 RDB 更适合用于备份数据库(AOF 在不断变化,不好备份),快速重启,而不会有 AOF 可能存在的潜在 bug,留着作为一个补救的手段。
当然,如果你只是用作缓存,只希望数据在程序运行的时候存在,那么就可以不使用任何持久化方式。
选项 | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全 | 丢数据 | 三种策略 |
附:配置文件详解
# redis进程是否以守护进程的方式运行,yes为是,no为否(不以守护进程的方式运行会占用一个终端)。
daemonize no
# 指定redis进程的PID文件存放位置
pidfile /var/run/redis.pid
# redis进程的端口号
port 6379
#是否开启保护模式,默认开启。要是配置里没有指定bind和密码。开启该参数后,redis只会本地进行访问,拒绝外部访问。要是开启了密码和bind,可以开启。否则最好关闭设置为no。
protected-mode yes
# 绑定的主机地址
bind 127.0.0.1
# 客户端闲置多长时间后关闭连接,默认此参数为0即关闭此功能
timeout 300
# redis日志级别,可用的级别有debug.verbose.notice.warning
loglevel verbose
# log文件输出位置,如果进程以守护进程的方式运行,此处又将输出文件设置为stdout的话,就会将日志信息输出到/dev/null里面去了
logfile stdout
# 设置数据库的数量,默认为0可以使用select <dbid>命令在连接上指定数据库id
databases 16
# 指定在多少时间内刷新次数达到多少的时候会将数据同步到数据文件
save <seconds> <changes>
# 指定存储至本地数据库时是否压缩文件,默认为yes即启用存储
rdbcompression yes
# 指定本地数据库文件名
dbfilename dump.db
# 指定本地数据问就按存放位置
dir ./
# 指定当本机为slave服务时,设置master服务的IP地址及端口,在redis启动的时候他会自动跟master进行数据同步
replicaof <masterip> <masterport>
# 当master设置了密码保护时,slave服务连接master的密码
masterauth <master-password>
# 设置redis连接密码,如果配置了连接密码,客户端在连接redis是需要通过AUTH<password>命令提供密码,默认关闭
requirepass footbared
# 设置同一时间最大客户连接数,默认无限制。redis可以同时连接的客户端数为redis程序可以打开的最大文件描述符,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis会关闭新的连接并向客户端返回 max number of clients reached 错误信息
maxclients 128
# 指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key。当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区
maxmemory<bytes>
# 指定是否在每次更新操作后进行日志记录,Redis在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为redis本身同步数据文件是按上面save条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为no。
appendonly no
# 指定跟新日志文件名默认为appendonly.aof
appendfilename appendonly.aof
# 指定更新日志的条件,有三个可选参数 - no:表示等操作系统进行数据缓存同步到磁盘(快),always:表示每次更新操作后手动调用fsync()将数据写到磁盘(慢,安全), everysec:表示每秒同步一次(折衷,默认值);
appendfsync everysec