Logo
活死人の行知路

Redis 持久化


📅 | 📝 1420 字
#redis #persistence #rdb #aof

这是 Redis 持久化的卡片盒笔记集合。每张卡片独立成篇,卡片之间通过 #锚点 互联。 遵循卢曼卡片盒笔记法:一张卡片 = 一个想法,用你自己的话写,像教别人一样。


卡片索引

卡片类型说明
什么是持久化概念卡持久化的定义与主流方式
RDB 是什么概念卡RDB 快照的原理与触发方式
save vs bgsave对比卡两种 RDB 触发命令的差异
RDB 配置与实验操作卡配置参数 + 完整实验
AOF 是什么概念卡AOF 日志原理与刷盘策略
AOF 重写概念卡AOF 重写机制与触发条件
AOF 演示操作卡开启 AOF + 查看文件 + 重写
RDB vs AOF 选择对比卡何时用 RDB、何时用 AOF
fork 与子进程优化故障卡fork 开销分析与优化方案

概念卡:什么是持久化?

一句话:Redis 数据在内存里,断电就没了。持久化就是把内存数据写到磁盘上,重启后能从磁盘恢复。

为什么需要持久化

Redis 把所有数据放在内存中,追求极致性能。但内存是易失的——进程崩溃、机器断电,数据就丢了。持久化解决的就是这个问题:

  1. 数据更新时,异步写入磁盘
  2. 磁盘数据在关机重启后依然存在
  3. 恢复时从磁盘加载到内存

主流数据库持久化方式

方式原理例子
快照某时间点对数据的完整备份MySQL Dump、Redis RDB
日志记录每次更新操作,恢复时回放日志MySQL Binlog、Redis AOF、HBase HLog

RDB 是快照,AOF 是日志。Redis 同时支持两种,也可以一起用。

相关卡片


概念卡:RDB 是什么?

一句话:RDB 就是一段时间点的内存完整快照(某一时刻的内存快照),像一个"照片",拍完就能随时恢复。

工作原理

内存中的数据 ──(bgsave)──> 二进制快照文件(.rdb) ──(加载)──> 重启后恢复内存

Redis 通过一条命令将内存中的数据生成一个二进制快照文件,保存到硬盘。恢复时直接把 RDB 文件加载到内存即可。

RDB 的三种触发方式

1.手动 save(同步)

  • 阻塞所有客户端命令,直到快照完成
  • 数据量大时可能造成严重延迟
  • 生产环境几乎不用

  • 策略:如果存在老的 RDB 文件,新文件会替换旧文件
  • 复杂度:O(n)

2.手动 bgsave(异步)

  • 立即返回 OK,在后台子进程中执行
  • 使用 Linux fork() 创建子进程生成 RDB
  • 子进程完成后通知主进程
  • 生产环境推荐的手动方式

  • 策略:如果存在老的 RDB 文件,新文件会替换旧文件
  • 复杂度:O(n)

3.自动触发

满足以下任一条件就会自动执行 bgsave:

savesecondschanges说明
save 900 19001900 秒内至少有 1 次写操作
save 300 1030010300 秒内至少有 10 次写操作
save 60 10000601000060 秒内至少有 10000 次写操作

默认三条都开启。生产环境通常全部注释掉,改为按需 bgsave。

缺点:频率过于频繁,我们无法控制,也可能造成硬盘压力。

RDB 的其他触发场景

  • 主从复制:Master 做全量复制时自动生成 RDB
  • shutdown:Redis 优雅关闭时自动生成
  • debug reload:重载配置时(等同于不清空内存的重启)

相关卡片


对比卡:save vs bgsave

一句话:save 是同步的,会卡住所有请求;bgsave 是异步的,后台悄悄做。

对比表

维度savebgsave
IO 类型同步异步
是否阻塞客户端是,阻塞直到完成否,立即返回
fork 阻塞不涉及 forkfork 瞬间会短暂阻塞(微秒级)
复杂度O(n)O(n)
额外内存不消耗fork 产生 CoW 开销
适用场景几乎不用生产环境推荐

为什么 bgsave 不阻塞客户端?

bgsave 的核心是 Linux 的 fork():

  1. 主进程 fork() 出一个子进程
  2. 子进程拥有主进程内存的只读副本(写时复制 Copy-On-Write)
  3. 子进程遍历内存,把数据写入 RDB 文件
  4. 主进程继续处理客户端请求
  5. 子进程完成后,将临时文件替换为正式 RDB 文件

⚠️ fork() 的瞬间会短暂阻塞主线程(复制内存页表),但时间极短(微秒级),通常可以忽略。阻塞时间取决于内存总量。

相关卡片


操作卡:RDB 配置与实验

一句话:关掉自动触发,手动 bgsave,看日志和文件。

RDB 核心配置

参数默认值说明
dbfilenamedump.rdbRDB 文件名
dir/RDB 文件存放目录
rdbcompressionyes是否压缩 RDB 文件
rdbchecksumyes是否校验 RDB 文件
stop-writes-on-bgsave-erroryesbgsave 出错时是否停止写入
save900 1 | 300 10 | 60 10000自动触发条件(生产建议注释掉)

实验:save 阻塞 vs bgsave 不阻塞

Step 1:准备环境

# 启动 Redis(RDB 自动触发已注释)
redis-server redis-6379.conf

# 初始状态
redis-cli
127.0.0.1:6379> dbsize
(integer) 0

Step 2:插入测试数据(约 1000 万条,占用 1.17G)

for ((i=0; i<20000000; i+=10000)); do
    redis-cli EVAL "
    for j = $i, $((i+9999)) do
        redis.call('SET', 'key' .. j, 'value' .. j)
    end
    " 0
done
redis-cli
127.0.0.1:6379> dbsize
(integer) 10000000

# 查看内存
127.0.0.1:6379> info memory
# Memory
used_memory:1255165992
used_memory_human:1.17G
used_memory_rss:1288101888
used_memory_peak:1316694544
used_memory_peak_human:1.23G
used_memory_lua:791552
mem_fragmentation_ratio:1.03
mem_allocator:jemalloc-3.6.0

Step 3:验证 save 阻塞

准备两个终端:

# 终端 1:执行 save
redis-cli SAVE

# 终端 2:在终端 1 执行 save 期间发请求
redis-cli
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
# ← 这个 get 会被阻塞,直到 save 完成才返回

data 目录查看 RDB 文件:

cd /opt/redis/data
ll -lh
total 237M
-rw-r--r-- 1 root root 1.8K 11月 12 15:19 6379.log
-rw-r--r-- 1 root root 237M 11月 12 15:19 dump-6379.rdb

Step 4:验证 bgsave 不阻塞

# 终端 1:执行 bgsave
redis-cli BGSAVE
Background saving started

# 终端 2:正常读写不受影响
redis-cli
127.0.0.1:6379> get hello
"world"   # ← 立即返回,没有被阻塞

查看 bgsave 日志:

451:M 12 Nov 15:22:58.686 * Background saving started by pid 2393083
2393083:C 12 Nov 15:23:02.671 * DB saved on disk
2393083:C 12 Nov 15:23:02.676 * RDB: 0 MB of memory used by copy-on-write
451:M 12 Nov 15:23:02.688 * Background saving terminated with success

查看子进程:

ps -ef | grep redis- | grep -v redis-cli | grep -v grep
root         451       1  1 13:21 ?        00:01:49 redis-server *:6379
root     2393094     451 76 15:26 ?        00:00:00 redis-rdb-bgsave *:6379

查看临时文件:

ll -lh
total 292M
-rw-r--r-- 1 root root 2.9K 11月 12 15:28 6379.log
-rw-r--r-- 1 root root 237M 11月 12 15:27 dump-6379.rdb
-rw-r--r-- 1 root root  56M 11月 12 15:28 temp-2393102.rdb   # ← 临时文件

bgsave 先生成临时文件,完成后原子替换为正式文件,保证不会读到损坏的 RDB。

Step 5:验证自动触发

# 修改配置,60 秒内 5 次写操作触发
save 60 5

# 重启
redis-cli shutdown
redis-server redis/config/redis-6379.conf

# 60 秒内写 6 次
set hello world
set a b
set c d
set e f
set g h
set i j

# 查看日志,自动触发 bgsave
tail -f 6379.log
2393125:M 12 Nov 15:34:17.496 * 5 changes in 60 seconds. Saving...
2393125:M 15:34:17.509 * Background saving started by pid 2393137
2393137:C 15:34:21.197 * DB saved on disk
2393137:C 15:34:21.201 * RDB: 0 MB of memory used by copy-on-write
2393125:M 15:34:21.282 * Background saving terminated with success

相关卡片


概念卡:AOF 是什么?

一句话:AOF 是"操作日志",每写一条命令就记一笔,重启时按顺序重放就能恢复数据。

RDB 的问题

问题说明
数据丢失窗口RDB 两次快照之间写入的数据会丢失(T2 ~ T4)
全量开销大每次都是 O(n) 全量快照,数据量大时耗时久

数据丢失场景:

时间点事件
T1执行多个写命令
T2满足 RDB 自动创建条件
T3再次执行多个写命令
T4宕机

T3 ~ T4 之间的数据会丢失。

AOF 原理

客户端写命令 ──> Redis 缓冲区 ──(fsync)──> 磁盘 AOF 文件
重启时 ──> 重放 AOF 文件中的所有命令 ──> 恢复数据

AOF 记录的是每一条写命令,而不是数据快照。恢复时逐条执行这些命令,就能重建完整数据。

AOF 三种刷盘策略

Redis 执行写命令时,先把命令写入缓冲区,再根据配置的策略刷新到磁盘:

策略行为数据安全性性能
always每条命令立即 fsync 到磁盘零丢失最差(磁盘只有几百 TPS)
everysec(推荐)每秒 fsync 一次最多丢 1 秒数据良好
no由操作系统决定何时刷盘不可控最好

生产推荐 everysec:性能和安全性最好的折中。最多丢失 1 秒数据,但性能接近直接写入。

相关卡片


概念卡:AOF 重写

一句话:AOF 文件会越来越大,重写就是"精简版"——去掉无效命令,只保留最终结果。

为什么要重写

AOF 记录了每一条写命令,随着时间推移文件会膨胀:

SET key1 val1
SET key1 val2     ← key1 被覆盖了,第一条是无效的
INCR counter      → 1
INCR counter      → 2
INCR counter      → 3
SET counter 3     ← 三条 INCR 可以合并成一条 SET

重写后:

SET key1 val2
SET counter 3

重写的两个好处:

  • 减少磁盘占用
  • 加速恢复速度(恢复时命令少了)

重写机制:bgrewriteaof

和 bgsave 一样,AOF 重写也使用 fork() 创建子进程:

  1. 主进程 fork() 子进程
  2. 子进程从内存中读取最新数据,生成精简的 AOF 文件(临时文件)
  3. 主进程继续处理请求,同时将新写入的命令追加到旧 AOF 和重写日志
  4. 子进程完成后,主进程将重写日志中的命令合并到新 AOF
  5. 替换旧 AOF 文件

AOF 重写触发条件

需要同时满足两个条件:

配置项默认值说明
auto-aof-rewrite-min-size64MBAOF 文件至少这么大才考虑重写
auto-aof-rewrite-percentage100%AOF 文件大小相比上次重写增长超过 100%

统计项:

统计项含义
aof_current_sizeAOF 当前尺寸(字节)
aof_base_sizeAOF 上次启动和重写时的尺寸(字节)

判断逻辑:

aof_current_size > auto-aof-rewrite-min-size
AND
(aof_current_size - aof_base_size) / aof_base_size > auto-aof-rewrite-percentage

流程

相关卡片


操作卡:AOF 演示

一句话:开启 AOF → 写几条数据 → 看文件 → 触发重写 → 再看文件。

开启 AOF

# 查看当前配置
127.0.0.1:6379> config get appendonly
1) "appendonly"
2) "no"

# 动态开启
127.0.0.1:6379> config set appendonly yes
OK

# 持久化到配置文件中
127.0.0.1:6379> config rewrite
OK
127.0.0.1:6379> exit

写入测试数据

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> set hello redis
OK
127.0.0.1:6379> set hello python
OK
127.0.0.1:6379> incr counter
(integer) 1
127.0.0.1:6379> incr counter
(integer) 2
127.0.0.1:6379> rpush list a
(integer) 1
127.0.0.1:6379> rpush list b
(integer) 2
127.0.0.1:6379> rpush list c
(integer) 3

查看 AOF 文件

Redis 4.0+ 使用文件重写机制(multi-part AOF),文件存放在 appendonlydir/ 目录下:

root@vm-1:/opt/redis/data# ll
total 505668
drwxr-xr-x  3 root   root        4096 Jul  1 15:17 ./
drwxrwxr-x 11 ubuntu ubuntu      4096 Jul  1 11:38 ../
-rw-r--r--  1 root   root        4762 Jul  1 15:17 6379.log
drwxr-xr-x  2 root   root        4096 Jul  1 15:15 appendonlydir/
-rw-r--r--  1 root   root   517777890 Jul  1 15:17 dump-6379.rdb

root@vm-1:/opt/redis/data# ll appendonlydir/
total 505664
drwxr-xr-x 2 root root      4096 Jul  1 15:15 ./
drwxr-xr-x 3 root root      4096 Jul  1 15:17 ../
-rw-r--r-- 1 root root 517777890 Jul  1 15:15 appendonly.aof.1.base.rdb    # 基础 RDB
-rw-r--r-- 1 root root       279 Jul  1 15:17 appendonly.aof.1.incr.aof    # 增量 AOF
-rw-r--r-- 1 root root       102 Jul  1 15:15 appendonly.aof.manifest      # 文件清单

Redis 4.0+ 的文件结构变化:不再是一个 appendonly.aof 文件,而是 appendonlydir/ 目录下的一组文件。base.rdb 是快照,incr.aof 是增量日志,manifest 描述文件顺序。

手动触发 AOF 重写

127.0.0.1:6379> bgrewriteaof
Background append only file rewriting started

重写后查看文件:

root@vm-1:/opt/redis/data/appendonlydir# ll
total 505660
drwxr-xr-x 2 root root      4096 Jul  1 15:21 ./
drwxr-xr-x 3 root root      4096 Jul  1 15:21 ../
-rw-r--r-- 1 root root 517777927 Jul  1 15:21 appendonly.aof.2.base.rdb    # ← 编号变了,文件更新了
-rw-r--r-- 1 root root         0 Jul  1 15:21 appendonly.aof.2.incr.aof    # ← 增量文件清空
-rw-r--r-- 1 root root       102 Jul  1 15:21 appendonly.aof.manifest

重写后增量 AOF 文件变为 0,因为所有有效命令已经合并到新的 base.rdb 中。后续写操作会生成新的 incr.aof。

相关卡片


对比卡:RDB vs AOF 怎么选?

一句话:RDB 快但可能丢数据,AOF 安全但文件大。生产环境两个都开,RDB 做备份,AOF 做恢复。

对比表

维度RDBAOF
持久化方式快照(完整数据)日志(操作命令)
恢复速度快(直接加载二进制文件)慢(逐条回放命令)
数据安全性差(两次快照间的数据会丢)好(最多丢 1 秒)
磁盘占用小(压缩后的二进制)大(每条命令都记录)
CPU 开销低(偶尔 bgsave)高(持续追加 + 重写)
适用场景容灾备份、大数据集恢复高可用、低延迟恢复

RDB vs AOF 对比图

RDB 最佳策略

  1. 推荐关闭自动触发(注释掉所有 save 行)
  2. 按天/小时手动 bgsave 做定期备份
  3. 主从模式下,从节点推荐开启 RDB(用于复制)

AOF 最佳策略

  1. 推荐开启,策略设为 everysec
  2. 让 Redis 自动管理 AOF 重写(默认配置即可)
  3. 监控 AOF 文件大小,必要时手动 bgrewriteaof

两者都开

  • Redis 重启时优先用 AOF 恢复(数据更全)
  • RDB 作为 AOF 的兜底备份

通用建议

  1. 分片:数据量大时做水平分片
  2. 定位:明确 Redis 是缓存还是持久存储
  3. 监控:硬盘、内存、负载、网络
  4. 容量:确保内存足够放下数据集

相关卡片


故障卡:fork 与子进程优化

一句话:bgsave 和 bgrewriteaof 都要 fork(),fork 的开销和内存量成正比,内存越大越慢。

fork 的本质

  • fork() 是同步操作,复制的是内存页表而非整个内存
  • 大部分情况下速度非常快(微秒级)
  • 但如果内存很大或系统负载高,fork 可能变得很慢,甚至阻塞主线程

如何查看 fork 耗时

127.0.0.1:6379> info persistence | grep latest_fork
latest_fork_usec:12345

latest_fork_usec 单位是微秒,超过 100000(100ms)就需要关注了。

fork 开销的三大维度

1.CPU

  • 开销来源:RDB/AOF 文件生成是 CPU 密集型(内存 → 磁盘序列化)
  • 优化:
    • 不要把 Redis 进程绑定到单个 CPU 核心
    • 不要和 CPU 密集型应用部署在同一台机器

2.内存(CoW 开销)

  • 开销来源:fork 后子进程只读父进程内存,父进程写数据时触发 Copy-On-Write,产生额外内存
  • 优化:
    • 关闭透明大页:echo never > /sys/kernel/mm/transparent_hugepage/enabled
    • 设置 vm.overcommit_memory=1(允许 fork 时超额分配内存)
    • 控制 maxmemory,不要让内存无限增长

3.磁盘 IO

  • 开销来源:AOF/RDB 文件写入
  • 优化:
    • 不要用高 IO 负载的服务器同时跑 Redis
    • 设置 no-appendfsync-on-rewrite yes(重写期间暂停 fsync,减少磁盘压力)
    • 使用 SSD 提升写入性能
    • 结合 iostat、iotop 分析磁盘瓶颈

降低 fork 频率的方法

方法说明
放宽 AOF 重写触发阈值增大 auto-aof-rewrite-min-size
减少不必要的全量复制优化主从配置,利用部分复制
控制实例内存上限合理设置 maxmemory
使用物理机或高效虚拟化KVM/Xen 的 fork 支持比某些云平台好

AOF 追加阻塞

相关卡片