Redis 持久化实战与原理详解
前言
当 Redis 仅用于缓存时,当缓存失效后,可以从关系型数据库中重新加载数据,不存在丢数据的问题。但 Redis 也可以作为操作数据库,存储业务数据,因此需要一定的机制来保证在极端情况下数据尽量不丢失,比如意外断电等灾难场景
生产环境下,为了保证服务可用性,会将 Redis 以高可用的方式部署,如主从复制、哨兵模式、集群模式。但意外总是那么不经意,极端情况下,仍然会存在数据丢失的风险。因此持久化是必不可少的能力
开启持久化功能后,当 Redis 服务器重启时,内存中的数据已经丢失,可以从备份的磁盘文件中恢复
Redis 支持 RDB 和 AOF 两种持久化方式,并衍生出以下 4 种持久化策略
- RDB(Redis Database)
- AOF(Append Only File)
- 不开启持久化
- RBD 与 AOF 结合
AOF 持久化更可靠,但需要消耗更多的资源,RDB 消耗的资源较少,但持久化没有 AOF 可靠。实际选型时可以按具体情况权衡利弊。Redis 官方更推荐使用 AOF,或者 RDB + AOF
的持久化策略
一、RDB
实现方式
RDB 持久化以一定的时间间隔,对内存中的数据打一份实时快照,并存到一个磁盘文件中,默认配置下,该文件名为 dump.rdb
启用方式
开启 RDB 持久化的配置非常简单,语法如下
save <seconds> <changes> [<seconds> <changes> ...]
注意:一个 <seconds> <changes>
对就是一个保存点,可以配置多个保存点
这是 Redis 的默认持久化策略,Redis7.0 及以上,如果不显示配置 save
,Redis 的默认配置如下
save 3600 1 300 100 60 10000
默认配置下,有多个保存点
- 3600s(1 小时)至少有 1 次修改(Redis7.0 以下是 900s,即 15 分钟至少有 1 次修改)
- 300s(5 分钟) 至少有 100 次修改
- 60s(1 分钟) 至少有 10000 次修改
此外,在 Redis 正常停机之前,会尝试执行一次全量的快照持久化,尽量保证所有的数据都被写入到 RDB 文件中
优缺点
优点
- RDB 是一个紧凑的单文件,是存储时间点全量内存数据的实时表示,是数据备份的首选方式
- RDB 非常适合作为灾难恢复方案,因为只有一个相对小的单文件,有利于远程传输
- RDB 持久化方式性能好,Redis 父进程会 fork 一个子进程完成持久化处理,父进程自身没有磁盘 I/O 操作
- 数据量极大时,Redis 通过持久化文件启动时,RDB 比 AOF 更快
- 在主从复制场景下,RDB 支持部分重新同步。在从节点重启或者故障转移后,从节点重新连接到主节点时,可以只请求主节点发送部分数据,而不是全部数据。这样可以减少重新同步所需的带宽和时间
缺点
- 相对于 AOF,RDB 持久化方式丢数据的风险更大。假设配置的最小保存点间隔为 1min,Redis 每 1 分钟保存一次快照,那么在 Redis 意外停止时,最多可能丢失近 1 分钟的数据
- 由于 RDB 需要 fork 子进程来完成持久化处理,如果数据量很大,这个处理过程很耗时。严重情况下,甚至会有高达 1s 的业务延迟。AOF 虽然也会 fork 子进程,但是频率比较低
二、AOF
Redis1.1 引入了 AOF(Append Only File),从字面上理解,就是持续往一个文件追加操作日志
实现方式
Redis 按照 Redis 协议的格式记录每一次写操作日志。在服务重启时,重放这些日志,重建历史数据
fsync
待持久化的 AOF 记录会先存到输出缓冲区,Redis 会调用 fsync()
方法,请求操作系统将缓冲区的数据刷盘
Redis 支持以下 3 种 fsync 模式
fsync 模式 | 描述 |
---|---|
always | 每一次写操作都会调用 fsync() 方法,将记录刷到磁盘文件中。速度很慢,但是最安全,几乎不会出现数据丢失 |
everysec | 默认模式。每 1s 调用一次 fsync() 方法,将输出缓冲区中积累的记录刷到磁盘文件中。速度和安全性适中 |
no | Redis 不主动调用 fsync() 方法,由操作系统决定什么时候刷盘。速度更快,但数据安全性最低 |
启用方式
Redis 默认关闭 AOF 持久化功能。启用方式如下,在 redis.conf
文件中修改
appendonly yes
appendfsync everysec
优缺点
优点
- AOF 持久化更可靠。fsync 默认配置 everysec 模式下,仍然有良好的写入性能,fsync 由后台线程处理,主线程则专注于写操作。这种 fsync 模式下,最多会丢失 1s 的写入数据
- AOF 文件只支持追加模式,即使由于磁盘已被写满,导致文件以未完成的写入命令结束,文件有损坏的情况,也可以通过
redis-check-aof
工具轻松修复 - 当 AOF 文件变得很大时,Redis 会自动在后台进行文件压缩重写,也就是
rewrite
,这个过程是安全的。当触发重写条件时,会创建一个新 AOF 文件,然后在后台以重建当前数据最精简的写入命令追加到新文件,处理过程结束后将新文件替换旧文件,新的数据写入日志会追加到重写后的文件中 - AOF 文件内容是一条一条的操作日志,通俗易懂,且很容易解析
缺点
- 相同数据量下,AOF 文件通常比 RDB 文件大
- fsync 模式为
always
和everysec
时,AOF 在运行效率通常比 RDB 慢
相关配置
AOF 配置 | 描述 |
---|---|
appendonly | 是否启用 AOF 持久化,默认值为 no ,不启用,如需启用,设为 yes |
appendfsync | 设置 fsync 模式,默认值为 everysec ,其他可选值为 always , no |
appendfilename | AOF 文件名/前缀,默认值为 appendonly.aof 。Redis7.0 以下,仅表示文件名,Redis7.0+,表示 AOF 相关文件的前缀 |
appenddirname | Redis7.0 新增,AOF 文件夹,用于放置所有 AOF 相关文件。默认值为 appendonlydir |
no-appendfsync-on-rewrite | 重写期间是否允许执行同步操作,默认值为 no 。启用的主要目的是提高 AOF 重写的性能 |
auto-aof-rewrite-percentage | 自动重写的触发百分比,默认值为 100。当 AOF 文件大小超过上次重写后的大小的一定百分比时,将触发自动重写。自动重写底层调用 BGREWRITEAOF 命令。如果设置为 0,将彻底关闭自动重写 |
auto-aof-rewrite-min-size | 触发重写的最小文件大小,默认值为 64MB。当 AOF 文件大小超过此值时,将触发自动重写 |
aof-load-truncated | 启动时检测到 AOF 文件被截断时的行为,默认值为 yes ,Redis 将尝试加载截断的 AOF 文件,这可能会丢失部分数据;如果设置为 no ,Redis 将拒绝启动并给出错误提示 |
aof-use-rdb-preamble | 在 AOF 文件中使用 RDB 文件头部信息,默认值为 yes 。如果设置为 yes ,Redis 在 AOF 文件开头添加 RDB 文件的内容,以便于在启动时快速加载数据;如果设置为 no ,则不添加 RDB 文件头部信息,节省空间但可能导致启动时间较长 |
aof-timestamp-enabled | Redis7.0 新增,默认值为no 。如果启用,在 AOF 文件中记录每个写命令的时间戳信息。但 Redis7.0 以下的 Redis 可能解析不了这种格式,有兼容性问题 |
aof-rewrite-incremental-fsync | 重写时是否采用增量同步的方式。默认值为 yes ,在重写过程中会以 4MB 的增量同步,可以降低延迟;如果停用,则在 AOF 重写完成后才执行一次完全同步 |
三、实战
准备 Redis 环境
使用 Docker 部署 Redis7.2,假设数据和配置都放在 ~/redis
目录下
mkdir ~/redis
mkdir ~/redis/data
mkdir ~/redis/conf
touch ~/redis/conf/redis.conf
拉取镜像
docker pull redis:7.2
docker run --name redis -p 6379:6379 \
-v ~/redis/data:/data \
-v ~/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis:7.2 redis-server /etc/redis/redis.conf
1. 不开启持久化功能
默认配置下,AOF 持久化功能不开启,在 ~/redis/conf/redis.conf
中填入如下内容,关闭 RDB 持久化,就相当于不开启持久化功能,服务重启后,数据就丢失了
save ""
启动 Redis,在 redis-cli
中简单插入几个字符串
> keys *
(empty list or set)
> set test_case no_persistence
"OK"
> set data_type str
"OK"
> keys *
1) "data_type"
2) "test_case"
查看 ~/redis/data
目录,发现没有生成 dump.rdb
文件。重启容器
docker restart redis
继续在 redis-cli
中观察,可以看到刚才的 key 已经不存在了
> keys *
(empty list or set)
2. 开启 RDB 持久化
在 ~/redis/conf/redis.conf
中填入如下内容,开启 RDB 持久化,3s 内至少有 1 次修改操作时,触发一次快照持久化
save 3 1
使用 docker restart redis
命令重启 redis 容器
然后编写如下 Python 测试脚本
import os
import redis
import time
import logging
def set_string(redis_conn, string_key: str):
logging.info(f"set key: {string_key}")
redis_conn.set(string_key, "value")
def set_strings_within_seconds(redis_conn, strings: list, n_seconds: int):
start = time.time()
for key_item in strings:
set_string(redis_conn, key_item)
elapsed = time.time() - start
if n_seconds > elapsed:
time.sleep(n_seconds - elapsed)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
r = redis.Redis(host="localhost", port=6379, db=0)
keys = r.keys()
logging.info(f"Init keys in memory: {len(keys)}")
set_strings_within_seconds(r, ["string_1", "string_2"], 3)
set_strings_within_seconds(r, ["string_3", "string_4"], 3)
set_string(r, "string_5")
set_string(r, "string_6")
keys = r.keys()
# Kill redis container immediately, mock power outage.
os.system("docker kill redis")
logging.info("Oops! Power outage ...")
logging.info(f"Before killing redis, keys in memory: {len(keys)}")
# Restart redis container.
os.system("docker start redis")
time.sleep(3)
keys = r.keys()
logging.info(f"After restarting redis, keys in memory: {len(keys)}")
输出日志如下
2024-04-22 13:02:52,187 - INFO - Init keys in memory: 0
2024-04-22 13:02:52,187 - INFO - set key: string_1
2024-04-22 13:02:52,188 - INFO - set key: string_2
2024-04-22 13:02:55,188 - INFO - set key: string_3
2024-04-22 13:02:55,190 - INFO - set key: string_4
2024-04-22 13:02:58,193 - INFO - set key: string_5
2024-04-22 13:02:58,196 - INFO - set key: string_6
redis
2024-04-22 13:02:58,623 - INFO - Oops! Power outage ...
2024-04-22 13:02:58,623 - INFO - Before killing redis, keys in memory: 6
redis
2024-04-22 13:03:02,031 - INFO - After restarting redis, keys in memory: 4
0-3s 写入了 2 个字符串 string_1
和 string_2
,第 3s 触发一次 RDB 持久化
3-6s 写入了 2 个字符串 string_3
和 string_4
,第 6s 触发一次 RDB 持久化
第 6s+ 快速写入 2 个字符串 string_5
和 string_6
,此时内存中共有 6 个字符串。然后立即使用 docker kill redis
停止了容器,模拟一次断电,此时还没来得及触发 RDB 持久化。重新启动后,内存中只有 4 个字符串,此时已经出现了数据丢失
查看 ~/redis/data
目录,发现已经有 dump.rdb
文件了
3. 开启 AOF 持久化
在 ~/redis/conf/redis.conf
中填入如下内容,关闭 RDB 持久化,开启 AOF 持久化,并且每 1s 触发一次刷盘操作,当 AOF 文件达到 2MB 时,触发 rewrite 操作
save ""
appendonly yes
appendfsync everysec
auto-aof-rewrite-min-size 2mb
删除 ~/redis/data/dump.rdb
,然后执行如下命令清空数据
docker kill redis && docker start redis
重启 redis 容器后,可以发现 data
目录下多了 AOF 持久化相关的文件
.
└── appendonlydir
├── appendonly.aof.1.base.rdb
├── appendonly.aof.1.incr.aof
└── appendonly.aof.manifest
appendonly.aof.1.incr.aof
就是 AOF 文件,此时内容为空,接下来尝试写入一个字符串
> set string_1 value
"OK"
再次查看 appendonly.aof.1.incr.aof
文件,发现已经追加了很多内容
*2
$6
SELECT
$1
0
*3
$3
set
$8
string_1
$5
value
可以看到,其中包含 string_1
和 value
接下来再编写一段 Python 脚本,重复设置字符串 string_1
,让 AOF 文件逐渐变大,触发 rewrite
import redis
if __name__ == "__main__":
r = redis.Redis(host="localhost", port=6379, db=0)
for i in range(1, 50000):
r.set("string_1", f"value_{i}")
r.close()
执行结束后,查看 data
目录
.
└── appendonlydir
├── appendonly.aof.2.base.rdb
├── appendonly.aof.2.incr.aof
└── appendonly.aof.manifest
发现序号已经变成 2 了,appendonly.aof.2.base.rdb
中保存着 rewrite 后精简的写入命令
REDIS0011? redis-ver7.2.4?
redis-bits?@?ctime???%fused-mem???aof-base??string_1
value_46893?r???V?? **%**
appendonly.aof.2.incr.aof
前 12 行如下
*2
$6
SELECT
$1
0
*3
$3
SET
$8
string_1
$11
value_46894
通过以上 2 个文件,可以看出重写之前最后写入的 string_1
的值为 value_46893
,重写期间持续写入的 string_1
的值为 value_46894
4. 同时开启 RDB 和 AOF 持久化
可以同时使用 RDB 和 AOF 持久化业务数据。RDB 可以提供定期的完整数据库备份;AOF 则可以记录业务数据的修改操作,以确保业务数据的增量持久化和恢复能力。RDB 定期数据备份需要结合 cron 定时任务
在 ~/redis/conf/redis.conf
中填入如下内容。开启 RDB 持久化,每 1 小时进行一次数据备份。同时开启 AOF 持久化,并且每 1s 触发一次 AOF 刷盘操作
save 3600 1 300 100
appendonly yes
appendfsync everysec
删除 ~/redis/data/appendonlydir
,然后执行如下命令清空数据
docker kill redis && docker start redis
在 redis-cli
中写入 3 个字符串
> set string_1 value1
"OK"
> set string_2 value2
"OK"
> set string_3 value3
"OK"
然后重启 redis 容器,观察 data
目录,可以发现同时存在 RDB 文件和 AOF 文件
.
├── appendonlydir
│ ├── appendonly.aof.1.base.rdb
│ ├── appendonly.aof.1.incr.aof
│ └── appendonly.aof.manifest
└── dump.rdb
四、参考文档
转载自:https://juejin.cn/post/7360486735799435300