持久化
Redis 是一个内存数据库,所有数据默认存储在内存中。一旦进程退出或服务器断电,所有数据将不可逆转地丢失。持久化(Persistence)就是将内存中的数据写入磁盘,以便在重启后恢复。
1. 持久化概述
1.1 为什么需要持久化
内存(RAM)是易失性存储——断电即丢。Redis 的高性能正来源于纯内存操作,但这也带来一个根本问题:数据安全。持久化就是在"高性能"和"数据安全"之间寻找平衡点的机制。
1.2 三种持久化方案
Redis 提供了三种持久化方案,可以单独使用或组合使用:
| 方案 | 原理 | 引入版本 | 数据丢失窗口 |
|---|---|---|---|
| RDB (Redis Database Backup) | 内存快照,某一时刻的全量数据拷贝 | Redis 1.0 | 两次快照之间的所有写入 |
| AOF (Append Only File) | 记录每条写命令到日志,重启时重放 | Redis 1.0 | 取决于 appendfsync 策略 |
| 混合持久化 (RDB + AOF) | AOF 文件前半部分为 RDB 快照,后半部分为增量 AOF 命令 | Redis 4.0 | 取决于 appendfsync 策略 |
此外,Redis 还支持 No Persistence 模式——完全关闭持久化,数据仅在内存中存在,适合纯缓存场景。
1.3 各版本重大变更
| 版本 | 变更 |
|---|---|
| Redis 1.0 | RDB 和 AOF 基础支持 |
| Redis 2.8 | 自动 RDB 恢复检测、AOF 重写改进 |
| Redis 4.0 | 混合持久化(aof-use-rdb-preamble) |
| Redis 7.0 | Multi-Part AOF(appenddirname 目录结构) |
2. RDB (Redis Database Backup)
2.1 原理
RDB 持久化在某个时间点对 Redis 中的所有数据生成一个二进制压缩快照(snapshot),保存到磁盘。它不是一条一条记录写命令,而是直接保存当前内存中所有键值对的完整状态。
🔬 深入原理:Fork 子进程 + Copy-On-Write (COW)
RDB 不能由主进程直接执行(否则会长时间阻塞所有请求),因此 Redis 采用 fork 子进程的方案:
┌──────────────────┐ fork() ┌──────────────────┐
│ Redis 主进程 │ ──────────────► │ 子进程 │
│ (继续处理请求) │ │ (写入RDB文件) │
│ │ │ │
│ 共享物理内存页 │ ◄──── COW ───► │ 共享物理内存页 │
└──────────────────┘ └──────────────────┘COW 机制详解:
- fork() 调用:父进程调用
fork()创建子进程。OS 内核会为子进程创建页表(Page Table)副本,但父子进程共享同一份物理内存页。此时并没有发生实际的内存复制,仅复制了页表条目(约占用父进程内存的 0.1%-0.2%)。 - 页表标记为只读:所有共享的内存页被标记为 Copy-On-Write。
- 写入时复制:当父进程(Redis 主进程)收到写请求,需要修改某个 key 对应的内存页时,OS 会先复制该页的原始内容给子进程使用,然后让父进程修改新的副本。只有被修改的页才会被复制,未修改的页继续共享。
- 子进程只读:子进程只遍历内存数据并写入磁盘,不会修改任何内存,因此子进程一侧永远不会触发 COW。
COW 对内存的消耗估算:
额外内存 ≈ fork 时内存 × 写入比例 × 页表开销- 页表开销:约 0.1%-0.2% 的父进程内存(例如 10GB 实例约需 10-20MB 页表)。
- 写入比例:取决于 RDB 生成期间(通常数分钟至数十分钟)有多少内存页被修改。
- 保守建议:预留 30%-50% 的内存余量以应对极端写入场景。
🚨 陷阱:如果 Redis 在写密集场景下 fork,且剩余可用内存不足,可能触发 OOM Killer 被系统杀掉。
2.2 触发方式
RDB 快照有四种触发方式:
2.2.1 SAVE —— 同步阻塞
redis-cli> SAVE
OK- 执行者:Redis 主进程
- 行为:在主进程中直接执行 RDB 写入,期间阻塞所有客户端请求
- 耗时:取决于数据量,大数据集可能需要数秒到数分钟
- 适用场景:仅适合在关闭节点前做最后一次保存,或低峰期手动备份
🚨 陷阱:
SAVE在生产环境几乎不应使用——它会阻塞整个实例,导致所有客户端超时。除非你明确知道自己在做什么,否则永远用BGSAVE。
2.2.2 BGSAVE —— 异步非阻塞
redis-cli> BGSAVE
Background saving started执行流程:
客户端请求 BGSAVE
│
▼
主进程 fork() 创建子进程
│
├── 父进程:继续处理客户端请求
│
└── 子进程:遍历内存数据 → 写入临时文件 → rename() 原子替换 dump.rdb- fork 阶段:主进程会短暂阻塞(时间取决于内存大小,10GB 约需 50-200ms)
- 写入阶段:子进程执行,主进程不阻塞
- 原子替换:子进程先写入临时文件(
temp-<pid>.rdb),完成后通过rename(2)系统调用原子替换旧的dump.rdb - 数据一致性:由于 COW,RDB 文件代表 fork 那一瞬间的数据状态
INFO 命令查看状态:
redis-cli> INFO persistence
# Persistence
loading:0 # 是否正在加载
rdb_bgsave_in_progress:0 # 是否正在 BGSAVE
rdb_last_bgsave_status:ok # 上次 BGSAVE 结果
rdb_last_bgsave_time_sec:3 # 上次 BGSAVE 耗时(秒)
rdb_current_bgsave_time_sec:-1
rdb_last_cow_size:02.2.3 自动触发 —— save 配置
在 redis.conf 中配置条件自动触发 BGSAVE:
save 900 1 # 900 秒(15 分钟)内至少 1 个 key 被修改
save 300 10 # 300 秒(5 分钟)内至少 10 个 key 被修改
save 60 10000 # 60 秒内至少 10000 个 key 被修改- 多条
save配置是或关系,任意一条满足即触发 BGSAVE。 - Redis 内部维护一个
dirty计数器(记录上次保存后的修改次数)和一个lastsave时间戳。 serverCron函数每 100ms 检查一次是否满足条件。- 配置
save ""可以禁用自动 RDB。
save "" # 禁用自动 RDB 保存2.2.4 其他触发场景
| 触发场景 | 执行命令 | 说明 |
|---|---|---|
SHUTDOWN 时 |
SHUTDOWN SAVE |
关闭前自动执行一次 BGSAVE(若 AOF 未启用) |
| 主从全量同步 | 主节点自动 BGSAVE | Master 为每个 Slave 执行 BGSAVE 生成 RDB |
DEBUG RELOAD |
内部调用 | 调试用,重新加载 RDB |
FLUSHALL 后 |
不自动触发 | 手动执行 BGSAVE 否则 RDB 为空 ⚠️ |
2.3 配置参数详解
# /etc/redis/redis.conf 或 redis.conf
# ── RDB 触发条件 ──
save 900 1 # 15 分钟内至少 1 次修改
save 300 10 # 5 分钟内至少 10 次修改
save 60 10000 # 60 秒内至少 10000 次修改
save "" # 设为空字符串禁用自动 RDB
# ── 文件名与路径 ──
dbfilename dump.rdb # RDB 文件名
dir ./ # RDB 文件存储目录(同时影响 AOF)
# ── 压缩与校验 ──
rdbcompression yes # 是否使用 LZF 算法压缩字符串
# yes: 节省磁盘 ~60%,但增加 CPU 开销
# no: 文件更大,但 CPU 开销更低
rdbchecksum yes # 是否在 RDB 文件末尾写入 CRC64 校验和
# 用于检测文件是否损坏,写入和读取时均有开销
# ── 错误处理 ──
stop-writes-on-bgsave-error yes # BGSAVE 失败时是否拒绝写入
# yes: 阻止写入,防止进程悄悄丢失持久化
# no: 继续接受写入(风险:数据无法持久化)各参数深入说明:
| 参数 | 默认值 | 建议 | 说明 |
|---|---|---|---|
save |
三条规则 | 按写入量调整 | 写入量小可适当放宽,写入量大适当收紧 |
dbfilename |
dump.rdb |
保持默认 | 备份时建议加时间戳:dump_$(date +%Y%m%d).rdb |
dir |
./ |
设为独立目录 | 确保目录有足够空间和写入权限 |
rdbcompression |
yes |
yes |
CPU 不是瓶颈时开启,节省大量磁盘空间 |
rdbchecksum |
yes |
yes |
生产环境建议开启,能检测文件损坏 |
stop-writes-on-bgsave-error |
yes |
yes |
生产环境强烈建议开启 |
2.4 RDB 优缺点
| 优点 | 缺点 |
|---|---|
| 文件紧凑,适合备份和灾难恢复 | 两次 RDB 之间的数据可能丢失 |
| 恢复大数据集比 AOF 快(二进制直接加载) | fork 子进程耗时,内存越大越慢 |
| 对主进程性能影响小(子进程异步写入) | fork 时内存翻倍风险(COW + 页表) |
| 适合冷备、离线分析、数据迁移 | 频繁 fork 拷贝页表有 CPU 开销 |
| RDB 文件可用于主从同步 | 不是实时持久化,不适合高可靠性需求 |
⚡ 性能提示:BGSAVE 的 fork 阶段耗时主要由内存大小决定(而非 QPS 或键数量)。10GB 内存的实例,fork 约需要 50-200ms。如果对延迟极度敏感,应控制单实例内存 < 10GB,或使用 Redis Cluster 分片。
2.5 🔬 RDB 文件结构
RDB 文件是一个二进制格式文件,结构如下:
┌──────────────────────────────────────────────────┐
│ Header │ REDIS0009 (9 bytes) │
├──────────────────────────────────────────────────┤
│ Aux Fields │ redis-ver, redis-bits, ctime, │
│ │ used-mem 等辅助信息 │
├──────────────────────────────────────────────────┤
│ DB 0 │ SELECT 0 + Key-Value Pairs │
├──────────────────────────────────────────────────┤
│ DB 1 │ SELECT 1 + Key-Value Pairs │
├──────────────────────────────────────────────────┤
│ ... │ ... │
├──────────────────────────────────────────────────┤
│ DB N │ SELECT N + Key-Value Pairs │
├──────────────────────────────────────────────────┤
│ EOF │ 0xFF (1 byte) │
├──────────────────────────────────────────────────┤
│ Checksum │ CRC64 (8 bytes) │
└──────────────────────────────────────────────────┘各部分详解:
- 文件头(Header):固定字符串
REDIS+ 4 位版本号(如0009表示 RDB 版本 9),共 9 字节。 - 辅助字段(Aux Fields):Redis 版本、系统位数(32/64 位)、创建时间戳、内存使用量等元信息。
- 数据部分(Databases):按 DB 编号顺序排列,每个 DB 以
SELECTDB操作码开头,后跟键值对。 - 文件尾(EOF):固定字节
0xFF,标识数据部分结束。 - 校验和(Checksum):可选的 CRC64 校验和,从文件头到 EOF 的校验值。
每种数据类型的存储格式简述:
| Redis 类型 | RDB 编码 | 存储内容 |
|---|---|---|
| String | RDB_TYPE_STRING (0) |
直接存储字符串值 |
| List | RDB_TYPE_LIST (1) / RDB_TYPE_LIST_QUICKLIST (14) |
快速列表(Redis 3.2+ 使用 quicklist) |
| Set | RDB_TYPE_SET (2) / RDB_TYPE_SET_INTSET (11) |
小集合用 intset,大集合用 hashtable |
| ZSet | RDB_TYPE_ZSET (3) / RDB_TYPE_ZSET_ZIPLIST (12) |
小有序集合用 ziplist,大集合用 skiplist |
| Hash | RDB_TYPE_HASH (4) / RDB_TYPE_HASH_ZIPLIST (13) |
小哈希用 ziplist,大哈希用 hashtable |
| Stream | RDB_TYPE_STREAM_LISTPACKS (15) |
listpack 紧凑编码 |
3. AOF (Append Only File)
3.1 原理
AOF(Append Only File)持久化记录每一条写入命令到日志文件中,重启时按顺序**重放(replay)**所有命令来重建数据库状态。
与 RDB 的对比:
RDB: [数据] ──► dump.rdb (快照:保存数据本身)
AOF: [SET k v] ──► appendonly.aof (日志:保存生成数据的命令)工作流程:
Client Write Command
│
▼
┌──────────────────────────────────────────┐
│ 1. Redis 执行命令,修改内存数据 │
│ 2. 将命令以 RESP 协议格式追加到 │
│ AOF 缓冲区 (aof_buf) │
│ 3. 根据 appendfsync 策略调用 fsync() │
│ 将缓冲区内容写入磁盘 │
└──────────────────────────────────────────┘🔬 深入原理:写命令的数据流转
客户端写请求
│
▼
执行命令 (processCommand)
│
├──► 修改内存中的数据 (dbAdd / dbOverwrite 等)
│
└──► 如果 AOF 已开启:
│
▼
将命令写入 aof_buf (sds 缓冲区)
│
▼
事件循环 (beforeSleep)
│
▼
flushAppendOnlyFile()
│
├── appendfsync = always → write + fsync (每条命令)
├── appendfsync = everysec → write + 后台线程 fsync (每秒)
└── appendfsync = no → write (OS决定何时fsync)3.2 配置参数详解
# ── AOF 总开关 ──
appendonly yes # 开启 AOF 持久化
# 关闭时 (no): 只使用 RDB
# ── 文件名与目录 ──
appendfilename "appendonly.aof" # AOF 文件名 (7.0+ 为 base file 名称)
appenddirname "appendonlydir" # 7.0+ Multi-Part AOF 文件目录
# ── fsync 策略 ──
appendfsync everysec # 每秒 fsync 一次 (默认推荐)
# always: 每次写都 fsync
# no: 不主动 fsync
# ── Rewrite 相关 ──
no-appendfsync-on-rewrite yes # Rewrite 期间不执行 fsync
# yes: 由 OS 缓冲处理
# no: 正常 fsync(可能阻塞)
auto-aof-rewrite-percentage 100 # 当前 AOF 文件相对上次 Rewrite 后增长的百分比
auto-aof-rewrite-min-size 64mb # AOF 文件最小体积才触发 Rewrite
# ── 加载与恢复 ──
aof-load-truncated yes # 遇到尾部截断的 AOF 文件如何处理
# yes: 加载并警告
# no: 拒绝启动,需 redis-check-aof 修复
# ── 混合持久化 ──
aof-use-rdb-preamble yes # 是否在 AOF Rewrite 时使用 RDB 格式做前置内容
# yes: 混合持久化(推荐 4.0+)
# no: 纯 AOF 格式3.3 appendfsync 三种策略对比
AOF 文件写入涉及两个系统调用:write(2) 和 fsync(2)。
- write(2):将数据从用户态缓冲区写入内核的 page cache。此时数据尚未写入物理磁盘。
- fsync(2):将内核 page cache 中的数据强制刷入物理磁盘。这才是真正的持久化。
用户态 aof_buf
│ write(2)
▼
内核 page cache (断电丢失)
│ fsync(2)
▼
物理磁盘 (持久化)| 策略 | write | fsync | 数据丢失窗口 | 对性能影响 | 适用场景 |
|---|---|---|---|---|---|
| always | 每条命令 | 每条命令 | 最多 1 条命令 | 极大(磁盘速度成为瓶颈) | 金融、订单等极致可靠性要求 |
| everysec | 每条命令 | 每秒 1 次 | 最多 1 秒数据 | 较小(推荐) | 高可靠性要求的通用场景 |
| no | 每条命令 | 不调用 | OS 决定(通常 30s) | 几乎无影响 | 可容忍大量丢失的场景 |
💡 最佳实践:
everysec是生产环境的最佳平衡点——性能损失通常 < 5%,数据丢失窗口可控在 1 秒内。always仅在极端可靠性要求下使用,因为磁盘 IO 会成为瓶颈,单实例 QPS 可能从 10 万降到几千。
🔬 深入原理:everysec 的实现
┌────────────────────────────────────────────────┐
│ 主线程 后台 fsync 线程 │
│ │
│ write() 到 page cache ────────► fsync() 每秒 │
│ (每条命令都 write) │
│ │
│ 注意:如果后台线程上一次 fsync 尚未完成, │
│ 主线程可以选择: │
│ - 延迟 write(默认,最多延迟 2 秒) │
│ - 直接 write(由 OS 缓冲,有写入丢失风险) │
└────────────────────────────────────────────────┘3.4 AOF 重写 (Rewrite)
3.4.1 为什么需要重写
AOF 文件随着运行时间持续膨胀——它记录每一条写命令,但很多命令实际上被后续命令覆盖了:
# 没有 Rewrite 的 AOF 文件
SET counter 1 # 无效
SET counter 2 # 无效
SET counter 3 # 无效
...
SET counter 10000 # 前面 9999 条 SET counter 都是冗余的不重写的后果:
- 磁盘空间持续被占用,最终磁盘写满
- 重启恢复时需要重放大量冗余命令,恢复时间极长
- AOF 加载耗时可能远超 RDB
3.4.2 重写原理
AOF 重写不是分析旧 AOF 文件,而是直接读取当前内存数据,生成一条或多条能够重建这些数据的最精简命令。
┌───────────────┐ fork() ┌───────────────────┐
│ Redis 主进程 │ ──────────► │ 子进程 │
│ │ │ │
│ 持有最新内存 │ │ 读取当前内存数据 │
│ 数据状态 │ │ 生成精简 AOF 命令 │
│ │ │ 写入新 AOF 文件 │
└───────────────┘ └───────────────────┘重写规则示例:
| 原始 AOF 中的命令 | 重写后 | 说明 |
|---|---|---|
RPUSH list a; RPUSH list b; RPUSH list c |
RPUSH list a b c |
多条合并为一条 |
SET k 1; SET k 2; SET k 3 |
SET k 3 |
保留最终值 |
LPUSH list a; LTRIM list 0 9 |
生成对应的精简命令 | 保留内存中的最终状态 |
| 已过期的 key | 不写入 | 过期 key 不需要保存 |
INCR c * 100 |
SET c 100 |
用 SET 表达最终值 |
3.4.3 BGREWRITEAOF 流程
redis-cli> BGREWRITEAOF
Background append only file rewriting started完整流程:
客户端请求 BGREWRITEAOF
│
▼
主进程 fork() 创建子进程
│
├── 子进程:读取当前内存 → 生成精简命令 → 写入临时文件
│
├── 父进程:继续处理客户端请求
│ │
│ ├──► 写入旧的 AOF 缓冲区(正常 AOF 写入)
│ └──► 写入 AOF 重写缓冲区(收集子进程重写期间的增量命令)
│
▼(子进程重写完成)
父进程将 AOF 重写缓冲区中的增量命令追加到临时文件末尾
│
▼
父进程通过 rename() 原子替换旧的 AOF 文件🔬 深入原理:重写缓冲区的双写机制
在子进程执行重写期间,主进程仍在接受写请求。这些写命令需要同时写入两个地方:
- AOF 缓冲区(aof_buf):正常流程,最终写入旧的 AOF 文件。
- AOF 重写缓冲区(aof_rewrite_buf):收集重写期间的增量命令。
子进程完成重写后,父进程将重写缓冲区内容追加到临时 AOF 文件末尾,然后原子替换。这样确保重写后的 AOF 文件包含 fork 时刻到重写完成之间的所有变更。
3.4.4 自动触发条件
两个参数必须同时满足才会自动触发 BGREWRITEAOF:
auto-aof-rewrite-percentage 100 # 当前 AOF 文件大小 / 上次 Rewrite 后大小 > 100%
# 即当前大小是上次的 2 倍以上
auto-aof-rewrite-min-size 64mb # AOF 文件必须大于 64MB 才触发计算逻辑(Redis 源码 serverCron 中):
// 伪代码
long long growth = current_aof_size - last_rewrite_size;
long long percentage = (growth * 100) / last_rewrite_size;
if (current_aof_size >= auto_aof_rewrite_min_size &&
percentage >= auto_aof_rewrite_percentage) {
// 触发 BGREWRITEAOF
}示例:
时间线
─────►
Rewrite 后: AOF = 64MB (last_rewrite_size = 64MB)
写入了 64MB: AOF = 128MB → growth = 100% → 满足触发条件
写入了 32MB: AOF = 160MB → growth = 150% → 再次满足3.5 🔬 Multi-Part AOF (Redis 7.0+)
Redis 7.0 重构了 AOF 持久化机制,引入了 Multi-Part AOF,将单文件 AOF 拆分为多文件管理。
3.5.1 旧版 AOF 的问题(7.0 之前)
appendonly.aof (单文件)
├── 正常写入的命令
├── Rewrite 后的精简命令
└── Rewrite 期间的增量命令
问题:单一文件管理不灵活,Rewrite 时必须整个文件替换3.5.2 Multi-Part AOF 目录结构
appendonlydir/ (AOF 目录)
├── appendonly.aof.1.base.rdb (Base file: RDB 格式快照)
├── appendonly.aof.1.incr.aof (Incr file: 增量 AOF 命令)
├── appendonly.aof.2.incr.aof (下一个增量文件)
└── appendonly.aof.manifest (清单文件: 元数据)各部分说明:
| 文件 | 作用 | 格式 |
|---|---|---|
*.base.rdb |
某个时间点的数据快照(类似 RDB) | RDB 二进制 |
*.incr.aof |
Base 之后的增量写命令 | AOF 文本命令 |
manifest |
记录 Base 和 Incr 文件的顺序及元信息 | 键值对文本 |
manifest 文件示例:
file appendonly.aof.1.base.rdb seq 1 type b
file appendonly.aof.1.incr.aof seq 1 type i
file appendonly.aof.2.incr.aof seq 2 type iseq:序号,决定加载顺序type:b= base (RDB),i= incr (增量 AOF)
3.5.3 Multi-Part AOF 的优势
| 优势 | 说明 |
|---|---|
| 更灵活的管理 | 可以单独管理、清理增量文件 |
| 减少 IO 压力 | Rewrite 时只需创建新的 Base + 新的 Incr,旧 Incr 可回收 |
| 更好的容错 | 单个增量文件损坏不影响 Base 部分的恢复 |
| 平滑的重写 | 不需要一次性替换整个大文件 |
⚡ 性能提示:Multi-Part AOF 在 Redis 7.0+ 默认启用(只要开启了 AOF),无需额外配置。升级到 7.0 后旧的单文件 AOF 会自动迁移。
3.6 AOF 修复
当 AOF 文件损坏(如 Redis 崩溃导致文件尾部不完整),Redis 提供了修复工具。
3.6.1 检查 AOF 文件
redis-check-aof appendonly.aof输出示例:
AOF analyzed: size=185433943, ok_up_to=185433944, diff=1
AOF is not valid. Use the --fix option.ok_up_to:最后一个有效字节的偏移量diff:之后无效的字节数
3.6.2 修复 AOF 文件
redis-check-aof --fix appendonly.aof修复原理:
有效命令1 | 有效命令2 | ... | 不完整命令(损坏)
│
redis-check-aof --fix
│
▼
有效命令1 | 有效命令2 | ... ← 截断损坏部分修复工具会:
- 逐条解析 AOF 命令直到遇到第一个不合法的命令
- 截断该命令及其之后的所有内容
- 写入修复后的文件
注意事项:
- 修复是丢弃策略——损坏命令对应的数据不可恢复
- 建议先备份原 AOF 文件再执行修复:
cp appendonly.aof appendonly.aof.bak redis-check-aof --fix appendonly.aof - 配合
aof-load-truncated yes可以让 Redis 自动处理轻微截断
4. 混合持久化 (RDB + AOF)
4.1 原理
Redis 4.0 引入了混合持久化,结合了 RDB 的快速恢复和 AOF 的低数据丢失。
纯 AOF 文件:
SET k v | INCR c | SET k2 v2 | ... ← 全是 AOF 命令
(恢复时需逐条重放,速度慢)
混合持久化文件:
┌──────────────────────┬──────────────────┐
│ RDB 格式快照 │ AOF 增量命令 │
│ (二进制、压缩) │ (RESP 协议文本) │
└──────────────────────┴──────────────────┘
← 加载时直接读入内存 → ← 重放这部分命令 →配置:
aof-use-rdb-preamble yes # 开启混合持久化 (Redis 4.0+ 默认 yes)4.2 工作流程
- 正常写入:数据通过 AOF 机制写入(
appendfsync everysec控制落盘频率)。 - AOF Rewrite 时:子进程将当前内存数据以 RDB 二进制格式写入 AOF 文件开头,然后追加 Rewrite 期间的增量 AOF 命令。
- 重启恢复:
- Redis 检测到 AOF 文件前半部分是 RDB 格式
- 直接用 RDB 方式加载前半部分(速度快)
- 再重放后半部分的增量 AOF 命令
4.3 优势总结
| 维度 | 纯 RDB | 纯 AOF | 混合持久化 |
|---|---|---|---|
| 恢复速度 | 快 | 慢(百万命令级极慢) | 快(RDB 部分快 + 少量 AOF 重放) |
| 数据丢失 | 多(快照间隔内) | 少(1 秒内) | 少(1 秒内) |
| 文件大小 | 小 | 大(膨胀后极巨大) | 介于两者之间 |
| Rewrite 后文件 | N/A | 所有命令以 AOF 格式 | RDB 压缩格式 + 增量 AOF |
| 兼容性 | 全版本 | 全版本 | 4.0+(低版本 Redis 无法恢复) |
💡 最佳实践:混合持久化 +
appendfsync everysec是生产环境的推荐配置。它平衡了恢复速度(RDB 的优势)和数据安全(AOF 的优势),且 Rewrite 后文件更小。
4.4 数据恢复优先级
当 Redis 启动时,数据恢复的优先级为:
检测 AOF 是否开启?
│
├── YES (appendonly yes)
│ └── 加载 AOF 文件 (包括混合持久化文件)
│
└── NO (appendonly no)
└── 加载 RDB 文件 (dump.rdb)总结:AOF 优先级 > RDB。即使同时存在 dump.rdb 和 appendonly.aof,只要 appendonly yes,Redis 就优先加载 AOF。
5. 持久化方案选择
5.1 场景推荐
| 场景 | 推荐方案 | 典型配置 |
|---|---|---|
| 纯缓存,数据可从 DB 重建 | 不持久化 | save "" + appendonly no |
| 一般业务(可接受 1s 丢失) | 混合持久化(推荐) | appendonly yes + aof-use-rdb-preamble yes + appendfsync everysec |
| 高可靠性(订单、金融) | AOF always | appendonly yes + appendfsync always + save ""(可选关闭 RDB) |
| 灾备/离线分析 | 纯 RDB + 定时备份 | appendonly no + 定时 crontab BGSAVE |
| 只读备份/主从同步 | 纯 RDB | appendonly no + 合理的 save 间隔 |
| 大内存实例 (> 20GB) | Redis Cluster + 混合持久化 | 分片后每个实例 < 10GB,fork 延迟可控 |
🚨 陷阱:如果数据可以从 MySQL/PostgreSQL 等持久存储重建,关闭持久化可以最大化性能。但需要确保故障恢复流程完善且缓存重建不会压垮后端数据库(缓存雪崩)。
5.2 💡 持久化最佳实践
5.2.1 方案选择原则
是否需要数据持久化?
├── NO → save "" + appendonly no (纯缓存)
└── YES → 可以容忍秒级丢失?
├── NO → appendfsync always (极致可靠)
└── YES → 混合持久化 + appendfsync everysec (推荐)5.2.2 RDB 备份策略
# crontab 示例:每小时执行 BGSAVE 并保留最近 24 份
0 * * * * redis-cli -h 127.0.0.1 -p 6379 BGSAVE
5 * * * * cp /var/lib/redis/dump.rdb /backup/redis/dump_$(date +\%Y\%m\%d\%H).rdb
# 保留最近 24 份,删除 24 小时前的
0 0 * * * find /backup/redis/ -name "dump_*.rdb" -mtime +1 -delete
# 每日备份到远程(scp / rsync / 对象存储)
0 6 * * * rsync -avz /var/lib/redis/dump.rdb backup-server:/backup/redis/5.2.3 AOF fsync 策略
appendfsync everysec # 首选,性能损失 < 5%,最多丢 1 秒5.2.4 内存与 fork 管理
| 建议 | 原因 |
|---|---|
| 单实例内存 < 10GB | fork 延迟通常在 100ms 左右;更大的实例 fork 可能达到秒级 |
| 预留 30%-50% 内存 | 给 COW 操作留出空间 |
| 使用 Redis Cluster 分片 | 大数据量时分片,每个实例内存可控 |
监控 latest_fork_usec |
INFO stats 中获取,若 > 1 秒需关注 |
# 查看最近 fork 耗时
redis-cli> INFO stats | grep latest_fork_usec
latest_fork_usec:15234 # 15ms 内正常,> 1s 需关注5.2.5 避免 100% CPU 时触发持久化
当 CPU 使用率接近 100% 时,BGSAVE/BGREWRITEAOF 的 fork 操作更容易失败或被内核调度延迟。建议:
- 部署 Redis 时预留 CPU 资源
- 避免在 CPU 高峰期执行 BGSAVE(或使用
save条件控制触发时机) - 使用
redis-cli --latency监控延迟
5.2.6 定期测试恢复
# 在生产环境的测试/备份节点上定期执行恢复演练
redis-check-rdb /var/lib/redis/dump.rdb
redis-check-aof /var/lib/redis/appendonly.aof
# 在隔离环境中启动 Redis 验证数据完整性
redis-server --port 6380 --dbfilename test_restore.rdb &
redis-cli -p 6380 DBSIZE # 验证数据量是否符合预期
redis-cli -p 6380 SHUTDOWN NOSAVE6. 备份与恢复
6.1 RDB 文件备份脚本
#!/bin/bash
# redis-rdb-backup.sh: RDB 文件定时备份脚本
REDIS_CLI="redis-cli -h 127.0.0.1 -p 6379"
REDIS_DATA_DIR="/var/lib/redis"
BACKUP_DIR="/backup/redis/rdb"
RETENTION_DAYS=7
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# 确保备份目录存在
mkdir -p "${BACKUP_DIR}"
# 方案1: 触发 BGSAVE 后备份
echo "$(date): Triggering BGSAVE..."
${REDIS_CLI} BGSAVE
# 等待 BGSAVE 完成
while [ "$(${REDIS_CLI} INFO persistence | grep rdb_bgsave_in_progress | cut -d: -f2)" != "0" ]; do
sleep 1
done
echo "$(date): BGSAVE completed. Copying file..."
cp "${REDIS_DATA_DIR}/dump.rdb" "${BACKUP_DIR}/dump_${TIMESTAMP}.rdb"
# 方案2: 直接复制当前 RDB(不触发新 BGSAVE,适合频繁备份)
# cp "${REDIS_DATA_DIR}/dump.rdb" "${BACKUP_DIR}/dump_${TIMESTAMP}.rdb"
# 压缩
gzip "${BACKUP_DIR}/dump_${TIMESTAMP}.rdb"
# 校验
redis-check-rdb "${BACKUP_DIR}/dump_${TIMESTAMP}.rdb.gz" 2>&1 | grep -q "Checksum OK"
if [ $? -eq 0 ]; then
echo "$(date): Backup verified successfully: dump_${TIMESTAMP}.rdb.gz"
else
echo "$(date): Backup verification FAILED!" >&2
rm -f "${BACKUP_DIR}/dump_${TIMESTAMP}.rdb.gz"
exit 1
fi
# 清理过期备份
find "${BACKUP_DIR}" -name "dump_*.rdb.gz" -mtime +${RETENTION_DAYS} -delete
echo "$(date): Cleaned up backups older than ${RETENTION_DAYS} days."
echo "$(date): Backup completed successfully."6.2 AOF 文件备份策略
AOF 文件备份与 RDB 不同——AOF 文件在不断写入,直接复制可能得到不完整的文件。
#!/bin/bash
# redis-aof-backup.sh
BACKUP_DIR="/backup/redis/aof"
AOF_DIR="/var/lib/redis/appendonlydir" # Redis 7.0+ 目录结构
AOF_FILE="/var/lib/redis/appendonly.aof" # Redis < 7.0 单文件
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p "${BACKUP_DIR}"
# Redis 7.0+: 备份 Multi-Part AOF 目录
if [ -d "${AOF_DIR}" ]; then
# 先触发 BGREWRITEAOF,让 AOF 文件进入"干净"状态
redis-cli BGREWRITEAOF
# 等待 Rewrite 完成
while redis-cli INFO persistence | grep -q "aof_rewrite_in_progress:1"; do
sleep 1
done
# 复制整个目录
cp -r "${AOF_DIR}" "${BACKUP_DIR}/aof_${TIMESTAMP}"
tar -czf "${BACKUP_DIR}/aof_${TIMESTAMP}.tar.gz" -C "${BACKUP_DIR}" "aof_${TIMESTAMP}"
rm -rf "${BACKUP_DIR}/aof_${TIMESTAMP}"
else
# 传统单文件模式
redis-cli BGREWRITEAOF
while redis-cli INFO persistence | grep -q "aof_rewrite_in_progress:1"; do
sleep 1
done
cp "${AOF_FILE}" "${BACKUP_DIR}/appendonly_${TIMESTAMP}.aof"
gzip "${BACKUP_DIR}/appendonly_${TIMESTAMP}.aof"
fi
echo "AOF backup completed: ${TIMESTAMP}"6.3 异地备份方案
| 方案 | 命令/工具 | 优点 | 缺点 |
|---|---|---|---|
| rsync | rsync -avz /backup/redis/ remote:/backup/ |
增量传输、带宽友好 | 需要配置 SSH 密钥 |
| 对象存储 | aws s3 cp / coscli cp / ossutil cp |
高持久性、多副本 | 需要网络带宽、潜在费用 |
| NFS 挂载 | 直接写入 NFS 目录 | 即时异地 | NFS 故障影响 Redis |
| 主从复制 | Master-Slave 自动同步 | 准实时、零侵入 | 无法恢复误删数据 |
⚠️ 注意:不要将持久化文件写到 NFS 挂载的目录。NFS 延迟或故障会导致 Redis 阻塞或 AOF 损坏。使用 dir 配置本地目录,备份脚本负责传输到异地。
6.4 恢复演练流程
1. 停服或准备验证节点
↓
2. 复制持久化文件到恢复目录
↓
3. 启动 Redis 测试实例
↓
4. 验证数据完整性和数量
↓
5. 记录恢复耗时#!/bin/bash
# redis-restore-drill.sh: 恢复演练脚本
RESTORE_DIR="/tmp/redis_restore_test"
RESTORE_PORT=16379
BACKUP_FILE="/backup/redis/rdb/dump_latest.rdb.gz"
# 1. 准备恢复目录
mkdir -p "${RESTORE_DIR}"
gunzip -c "${BACKUP_FILE}" > "${RESTORE_DIR}/dump.rdb"
# 2. 验证 RDB 文件完整性
redis-check-rdb "${RESTORE_DIR}/dump.rdb"
if [ $? -ne 0 ]; then
echo "RDB file corrupted! Aborting."
exit 1
fi
# 3. 启动测试 Redis 实例
echo "Starting test Redis instance on port ${RESTORE_PORT}..."
redis-server --port ${RESTORE_PORT} \
--dir "${RESTORE_DIR}" \
--dbfilename dump.rdb \
--appendonly no \
--daemonize yes \
--logfile "${RESTORE_DIR}/redis-restore.log"
sleep 3
# 4. 验证数据
KEY_COUNT=$(redis-cli -p ${RESTORE_PORT} DBSIZE)
echo "Restored database size: ${KEY_COUNT} keys"
# 抽样验证关键数据
redis-cli -p ${RESTORE_PORT} EXISTS user:10001
redis-cli -p ${RESTORE_PORT} GET config:app_version
# 5. 清理
redis-cli -p ${RESTORE_PORT} SHUTDOWN NOSAVE
rm -rf "${RESTORE_DIR}"
echo "Restore drill completed successfully."7. 数据恢复流程
7.1 RDB 恢复
# 1. 停止 Redis
redis-cli SHUTDOWN SAVE
# 2. 将备份的 RDB 文件放置到 dir 配置的目录
cp /backup/redis/rdb/dump_20260711_030000.rdb /var/lib/redis/dump.rdb
# 3. 确保权限正确
chown redis:redis /var/lib/redis/dump.rdb
chmod 644 /var/lib/redis/dump.rdb
# 4. 启动 Redis
redis-server /etc/redis/redis.confRDB 恢复注意事项:
- 恢复前确保 Redis 实例已完全停止(不要使用
SIGTERM强杀)。 - AOF 必须关闭(
appendonly no),否则 Redis 优先加载 AOF 文件。 - RDB 文件放在
dir配置项指定的目录下。
7.2 AOF 恢复
# 1. 先验证 AOF 文件完整性
redis-check-aof /backup/redis/aof/appendonly.aof
# 2. 停止 Redis
redis-cli SHUTDOWN NOSAVE
# 3. 放置 AOF 文件
# Redis 7.0+ Multi-Part AOF:
cp -r /backup/redis/aof/appendonlydir /var/lib/redis/appendonlydir
# Redis < 7.0 单文件 AOF:
cp /backup/redis/aof/appendonly.aof /var/lib/redis/appendonly.aof
# 4. 确保 AOF 开启
# redis.conf:
# appendonly yes
# appendfilename "appendonly.aof"
# 5. 启动 Redis
redis-server /etc/redis/redis.conf7.3 混合持久化文件恢复
混合持久化文件的恢复流程与纯 AOF 相同——Redis 会自动检测文件格式并选择最佳加载方式:
加载 AOF 文件
│
├── 检测到 RDB 前缀 (REDIS00xx 魔数)
│ └── 以 RDB 模式加载前半部分
│ 然后重放后半部分的增量 AOF 命令
│
└── 未检测到 RDB 前缀
└── 以纯 AOF 模式逐条重放不需要特殊配置,Redis 4.0+ 会自动处理。
7.4 优先级总结
Redis 启动时的数据加载优先级:
1. AOF 开启 (appendonly yes)
├── AOF 文件存在且有效 → 加载 AOF
│ ├── 混合持久化 → RDB 快读 + AOF 重放
│ └── 纯 AOF → 逐条重放
└── AOF 文件不存在 → 以空数据库启动
2. AOF 关闭 (appendonly no)
├── RDB 文件存在 → 加载 RDB
└── RDB 文件不存在 → 以空数据库启动7.5 恢复性能对比
| 数据规模 | RDB 恢复 | AOF 恢复 | 混合持久化恢复 |
|---|---|---|---|
| 1 万条 | ~0.1s | ~0.3s | ~0.1s |
| 100 万条 | ~2s | ~30s | ~2s |
| 1000 万条 | ~20s | ~5min | ~25s |
| 1 亿条 | ~3min | ~50min | ~3.5min |
实际恢复时间取决于 CPU、磁盘 IOPS、数据类型复杂度。表内数值仅供参考。
8. 🚨 常见陷阱
8.1 关闭时数据全部丢失
陷阱:Redis 实例运行期间没有开启任何持久化(save "" + appendonly no),SHUTDOWN 或进程崩溃后所有数据永久丢失。
解决:
- 如果数据需要保留,至少开启 RDB 或 AOF 之一。
- 即使是缓存,也要评估缓存重建的成本和下游数据库压力。
8.2 fork 子进程时的内存翻倍风险
陷阱:编写密集场景下,RDB 的 BGSAVE 或 AOF 的 BGREWRITEAOF 会在 fork 子进程后因 COW 导致内存使用激增。如果系统没有预留足够内存,会触发 OOM Killer 杀掉 Redis 进程。
假设 Redis 占用 10GB 内存,fork 后:
- 页表拷贝:约 20-200MB
- COW 写入:如果 fork 期间有 50% 的内存页被修改,需额外 5GB
总内存需求可达 15GB+解决:
- 系统内存至少为 Redis
maxmemory的 1.5 倍。 - 监控
used_memory和系统可用内存。 - 在低写入时段触发 BGSAVE(通过
save参数控制触发时间窗口)。 - 对大数据量实例使用 Redis Cluster 分片。
8.3 RDB save 配置过于频繁
陷阱:误设置为 save 1 1(每秒只要有一次修改就 BGSAVE),导致 fork 子进程的频率极高,CPU 和内存消耗持续走高。
# ❌ 错误配置
save 1 1 # 每秒有修改就触发,灾难性
# ✅ 合理配置
save 900 1 # 15 分钟一次修改就保存
save 300 10 # 5 分钟 10 次修改
save 60 10000 # 1 分钟 10000 次解决:根据数据重要性和写入量设置合理的 save 间隔。写入越密集,save 条件可以越宽松(Redis 自动 BGSAVE 频率自然就高)。
8.4 AOF Rewrite 期间的阻塞问题
陷阱:no-appendfsync-on-rewrite no(Rewrite 期间正常 fsync)可能导致 fsync 阻塞。
fsync 是一个昂贵的系统调用(尤其 HDD)
Rewrite 期间,父进程需要写入大量数据到 AOF
如果每次都 fsync,可能造成主线程阻塞,延迟飙升解决:
no-appendfsync-on-rewrite yes # Rewrite 期间不主动 fsync设置为 yes,Rewrite 期间写入数据暂存在 OS page cache 中,Rewrite 完成后再由 OS 刷新。最多丢失约 30 秒数据(OS 刷新间隔),但可以避免阻塞。
8.5 AOF 文件无限增长不触发 Rewrite
陷阱:auto-aof-rewrite-min-size 设置过大(如 10GB),导致 AOF 文件远大于该值但仍未触发自动重写。
# ❌ 错误:min-size 过高
auto-aof-rewrite-min-size 10gb # 10GB 才触发,可能永远达不到
# ✅ 建议
auto-aof-rewrite-min-size 64mb
auto-aof-rewrite-percentage 100解决:
- 设置合理的
auto-aof-rewrite-min-size(建议 64MB-1GB)。 - 监控
aof_current_size和aof_last_rewrite_time指标。 - 必要时手动执行
BGREWRITEAOF。 - 写入量极大时可适当降低
auto-aof-rewrite-percentage(如 50%)。
8.6 只开 RDB 不开 AOF
陷阱:只配置了 RDB(如 save 900 1),意外宕机时丢失了最近 900 秒(15 分钟)内的所有数据。
解决:对数据可靠性有要求的场景,开启 AOF(或混合持久化):
appendonly yes
aof-use-rdb-preamble yes
appendfsync everysec8.7 混合持久化文件的跨版本兼容性
陷阱:使用 Redis 4.0+ 的混合持久化生成的 AOF 文件,无法被 Redis 3.x 或更低版本加载。
# Redis 3.2 尝试加载混合持久化文件
# 错误: "Bad file format reading the append only file"解决:
- 升级到 Redis 4.0+ 再开启混合持久化。
- 迁移数据时使用
redis-cli --rdb导出标准 RDB 文件,或使用主从同步。 - 回滚方案:在低版本 Redis 上使用纯 RDB 恢复,或使用
BGREWRITEAOF并临时关闭aof-use-rdb-preamble生成纯 AOF 文件。
8.8 用 kill -9 强杀 Redis
陷阱:kill -9 强杀进程可能导致:
- AOF 文件尾部不完整(部分写入的命令)
appendfsync everysec下丢失最后一秒的数据
解决:
# ✅ 正确关闭方式
redis-cli SHUTDOWN SAVE # 先做 BGSAVE 再关闭
redis-cli SHUTDOWN NOSAVE # 不做 BGSAVE 直接关闭(如果开启了 AOF)
# ✅ 优雅停止
kill -SIGTERM <pid> # 等同 SHUTDOWN NOSAVE8.9 磁盘空间满导致持久化失败
陷阱:dir 所在磁盘分区空间耗尽了,BGSAVE/BGREWRITEAOF 因磁盘空间不足失败,但 Redis 主进程仍在正常运行,数据实际处于"无持久化保护"状态。
解决:
- 监控磁盘使用率(阈值 < 80%)。
- 配置
stop-writes-on-bgsave-error yes(默认),确保 BGSAVE 失败后 Redis 拒绝写入,强制运维人员介入,避免数据悄悄丢失。 - 定期清理旧的备份文件。
- AOF 模式开启 Rewrite,避免文件无限膨胀。
8.10 主从全量同步时的 BGSave 压力
陷阱:一个 Master 挂多个 Slave 同时做全量同步,或 Slave 反复断连触发全量同步,Master 频繁 fork 执行 BGSAVE。
解决:
- 控制 Slave 数量(建议不超过 3-5 个)。
- 确保网络稳定,避免 Slave 反复断连。
- 使用
repl-backlog-size设置合理大小(建议 64MB-256MB),让 Slave 短时间内重连时可以部分同步而不触发全量同步。 - Redis 2.8+ 支持无磁盘复制(
repl-diskless-sync),直接通过网络传输数据而不写磁盘 RDB。
快速参考卡片
┌─────────────────────────────────────────────────────────────┐
│ 持久化方案速查 │
├──────────┬──────────────────┬───────────────────────────────┤
│ 纯缓存 │ save "" │ appendonly no │
│ 推荐方案 │ 默认配置 │ 混合持久化 + everysec │
│ 高可靠 │ appendfsync always│ 丢失 <= 1 条命令 │
│ 灾备 │ 定时 BGSAVE │ 保留多份 RDB │
├──────────┴──────────────────┴───────────────────────────────┤
│ 关键命令 │
│ BGSAVE — 异步生成 RDB │
│ BGREWRITEAOF — 异步重写 AOF │
│ redis-check-rdb — 验证 RDB 文件 │
│ redis-check-aof — 验证/修复 AOF 文件 │
├─────────────────────────────────────────────────────────────┤
│ 关键指标 (INFO persistence) │
│ rdb_last_bgsave_time_sec — 上次 BGSAVE 耗时 │
│ aof_current_size — 当前 AOF 文件大小 │
│ aof_last_rewrite_time_sec — 上次 Rewrite 耗时 │
│ 关键指标 (INFO stats) │
│ latest_fork_usec — 最近 fork 耗时(微秒) │
└─────────────────────────────────────────────────────────────┘