第 3 章:Redis 持久化
学习目标
- 理解 RDB 和 AOF 两种持久化原理
- 学会配置和选择持久化策略
- 掌握混合持久化(RDB + AOF)
- 了解 Redis 重启恢复流程
一、为什么需要持久化
Redis 数据在内存,进程挂掉就丢了。持久化把数据写到磁盘,重启后能恢复。生产环境必须开启持久化,否则:
- 主从全挂 = 全丢
- 即使有主从,首次部署从空开始也是灾难
二、RDB(快照)
定时把内存数据整体写到 .rdb 文件。
触发方式
# 配置自动触发(redis.conf)
save 900 1 # 900 秒内至少 1 个 key 变更
save 300 10 # 300 秒内至少 10 个
save 60 10000 # 60 秒内至少 10000 个
# 手动触发
SAVE # 同步,阻塞主线程(生产禁用)
BGSAVE # fork 子进程异步,推荐save 900 1 怎么读:
"Redis 像个保安,守着门口,数客户进出的次数"
900 秒 = 保安值班的"一轮时长"(15 分钟)
1 = 这轮只要进来过 1 个客户 → 拍快照
00:00 保安开始值班
00:01 set user:1 "tom" ← 进出 1 次
00:03 set user:2 "jerry" ← 进出 2 次
00:15 值班结束:本轮 ≥ 1 次 → 拍快照 ✅
00:15 开始新一轮,本轮 0 次 → 不拍为什么3 条配置?:给3 个保安,不同时间段不同严格度(任一达标就拍):
save 60 10000 ← 1 号保安:1 分钟内 10000 次才拍(很宽松)
save 300 10 ← 2 号保安:5 分钟内 10 次才拍
save 900 1 ← 3 号保安:15 分钟内 1 次就拍(很敏感)配置触发永远走异步(BGSAVE)—— 不是 SAVE。Redis 设计上已经替你做了选择,因为配置本来就要"经常触发",如果走同步 SAVE,主线程会一直阻塞 → 服务废掉。
| 方式 | 主线程 | 触发 |
|---|---|---|
SAVE 命令 | 阻塞(同步) | 手动 |
BGSAVE 命令 | 异步(fork) | 手动 |
save 900 1 配置 | 异步(=BGSAVE) | 自动 |
自动触发 + 手动触发不冲突:
| 自动(配置) | 手动(运维) | |
|---|---|---|
| 谁触发 | Redis 内部 | 你 |
| 时机 | 达到阈值 | 需要时 |
| 适合 | 日常备份 | 上线前/迁移/关机 |
手动触发场景:
# 上线前:怕出问题先拍
redis-cli BGSAVE
# 跨实例迁移
redis-cli -h A BGSAVE
scp A:/data/dump.rdb B:/data/
# 关机前
redis-cli SHUTDOWN # 默认关机前拍
redis-cli SHUTDOWN NOSAVE # 关机不拍SAVE vs BGSAVE 比喻:
SAVE = 经理亲自下场盘点 → 期间关门不营业(生产禁用)
BGSAVE = 经理派助理盘点 → 营业照常(fork 阶段会卡,但比 SAVE 轻)实战调小内存卡顿:
# 大内存项目,改宽松点,降低 fork 频率
save 900 1 # 默认值
save 3600 100 # 加一条:1 小时 100 次才拍
# ↑ 从"几十秒一拍"降到"几小时一拍",fork 卡顿频率大幅降低优缺点
| 优点 | 缺点 |
|---|---|
| 文件紧凑,适合备份 | 会丢最近一次快照之后的数据 |
| 恢复速度快 | 大数据集 fork 会卡顿 |
⚠️ 坑 1:
BGSAVE用 fork 创建子进程,内存越大 fork 越慢(20GB 内存可能卡几秒),期间阻塞所有请求。
只用 RDB 最大的缺点:丢数据。
纯 RDB:
12:00 拍快照
12:05 Redis 挂了
→ 12:00 之后的数据全丢(5 分钟工作没了)
纯 AOF(everysec):
12:05 Redis 挂了
→ 最多丢 1 秒RDB 丢多少看 save 配置:
save 60 10000 # 默认:1 分钟 10000 次才拍 → 最多丢 1 分钟
save 300 10 # 5 分钟 10 次才拍 → 最多丢 5 分钟
# 写严(丢少):save 1 1 → 最多丢 1 秒,但 fork 频繁卡内存其他缺点(次要):
| 缺点 | 影响 |
|---|---|
| 丢数据 | 主要缺点 |
| 大内存 fork 卡 | BGSAVE 时阻塞(见坑 1) |
| 不适合写多场景 | 频繁 fork 浪费 |
什么时候只用 RDB 没问题?
# ① 纯缓存(丢了不心疼,如 session 临时数据)
# ② 写很少 + 内存小(如配置中心)
# ③ 备份专用(主实例开 AOF,这个纯备份)生产实际:两个都开(第 5 节混合持久化)。
三、AOF(追加日志)
把每条写命令追加到 .aof 文件,重启时回放恢复。
开启
# redis.conf
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec # 每秒刷盘(推荐)
# appendfsync always # 每条都刷,性能差
# appendfsync no # 由 OS 决定,可能丢很多一句话讲清 AOF:
把"每条写命令"全记下来,重启时照着"重新敲一遍"恢复数据。
SET user:1 "tom" → 写到 aof
SET user:2 "jerry" → 写到 aof
SET user:1 "alice" → 写到 aof
↓ Redis 挂了 → 重启 → 读 aof → 重敲一遍 → 恢复appendfsync 三档(类比记账):
| 模式 | 类比 | 优点 | 缺点 |
|---|---|---|---|
always | 改一份给一份成绩 | 不丢数据 | 慢,机械盘 QPS 卡到几百 |
everysec | 攒 1 秒统一公布 | 快 + 最多丢 1 秒 | 极端情况丢最后 1 秒 |
no | 想起来再记 | 最快 | 宕机可能丢很多 |
生产标准配置(就这 3 行):
appendonly yes
appendfsync everysec
# 其他默认就好重写(AOF 压缩)
AOF 文件会越来越大,Redis 提供 BGREWRITEAOF 自动压缩:
# 自动触发
auto-aof-rewrite-percentage 100 # 比上次重写增长 100% 触发
auto-aof-rewrite-min-size 64mb # 文件至少 64MB 才触发
# 手动
BGREWRITEAOF重写思路:根据当前数据状态,生成最简命令(一条 SET k v 替代一万次变更)。
自动重写(Redis 默认就开):
auto-aof-rewrite-percentage 100 # 增长比例:翻倍才触发
auto-aof-rewrite-min-size 64mb # 最小文件:小文件不重写两个阈值配合:
00:00 AOF 50MB → 不重写(< 64MB)
01:00 AOF 80MB → 80÷50 = 160% > 100% 且 > 64MB → 自动重写
02:00 AOF 60MB → 60÷50 = 120% > 100% 但 < 64MB → 不重写
03:00 AOF 70MB → 触发重写手动触发(运维场景):
redis-cli BGREWRITEAOF# 上线前、迁移前手动压一次实战 percentage 100 + min-size 64mb 是 Redis 默认,不用改。
优缺点
| 优点 | 缺点 |
|---|---|
| 最多丢 1 秒数据 | 文件大,恢复慢 |
| 可读,易调试 | 写放大,QPS 受限 |
⚠️ 坑 2:
appendfsync always写入磁盘,机械盘下 QPS 可能掉到 1000 以下,生产不要用。
四、RDB vs AOF 对比
| 维度 | RDB | AOF |
|---|---|---|
| 持久化粒度 | 时间间隔 | 每条命令 |
| 数据安全 | 可能丢 N 分钟 | 最多丢 1 秒 |
| 文件大小 | 小 | 大 |
| 恢复速度 | 快 | 慢 |
| 写性能 | 高 | 较低 |
不是二选一,可以同时开:
Redis 持久化 = 两个独立开关:
RDB:save 60 10000 # RDB 开关(默认开)
AOF:appendonly yes # AOF 开关(默认关)| 配置 | 适合 |
|---|---|
| 只 RDB | 备份、灾备 |
| 只 AOF | 数据安全 |
| 两个都开 | 生产推荐(混合持久化) |
两个都开时,Redis 重启用谁?
AOF 数据更全 → 优先用 AOF 恢复
RDB 数据更老 → 被忽略(⚠️ 见坑 4)生产推荐配置(混合持久化):
save 900 1 # RDB 自动触发
appendonly yes # AOF 开启
aof-use-rdb-preamble yes # AOF 文件前半段用 RDB 格式(混合)
appendfsync everysec混合效果:AOF 文件 = RDB 全量 + AOF 增量,重启先快加载 RDB,再回放 AOF。
新项目口诀:两个都开,别纠结。
五、混合持久化(Redis 4.0+ 推荐)
RDB 做全量,AOF 记录增量,兼顾速度和安全。
# redis.conf
aof-use-rdb-preamble yes原理:重写 AOF 时,文件前半段是 RDB 格式全量数据,后半段是 AOF 格式增量命令。重启先加载 RDB,再回放 AOF 增量。
用"相册+日记"类比:
相册 = RDB(快照):存的是"现状"的照片
日记 = AOF(追加):按天记的流水账,越来越长
没混合前(纯 AOF):
你的日记本:全是流水账(10 万条记录)
→ 搬家时:从头读 10 万条(慢)
混合后(aof-use-rdb-preamble yes):
你的日记本改成:
┌──────────────┬──────────────┐
│ 12月31日的照片 │ 12月31日的日记 │
│ (拍了一张全家福) │ (之后的几条小事) │
│ ← 反映"现在" │ ← 反映"之后" │
└──────────────┴──────────────┘
↑ 1 张照片(快) ↑ 几条日记(准确)
搬家时:看照片 + 看日记 = 又快又全所以这一行配置就是:
aof-use-rdb-preamble yes
# = AOF 文件的前半段用"快照"形式存(RDB 格式)
# = 后半段还是按命令形式存为什么这么设计?
问题:纯 AOF 是流水账,恢复慢(读 10 万条命令)解法:
把"当前状态"拍成图(RDB 格式)写到 AOF 前半段
之后的新增改动还是 AOF 格式追加在后半段
重启恢复:看图(快)+ 看日记(补全)Redis 4.0+ 默认 yes,没缺点,所以都开。
生产推荐配置:
appendonly yes
aof-use-rdb-preamble yes
appendfsync everysec六、Java 实战:配置 RedisTemplate
重要说明:持久化是 Redis 服务端的事(改 redis.conf),Java 端只管"怎么连 + 怎么序列化"。两者分工:
┌─────────────┐
│ redis.conf │ ← 决定"数据怎么存盘"(持久化)
│ (服务端) │
└──────┬──────┘
│ TCP 连接
↓
┌─────────────┐
│ Java 客户端 │ ← 决定"怎么发命令、怎么序列化"
└─────────────┘redis.conf 持久化配置(回顾):
# 1. RDB 触发规则
save 900 1
save 300 10
save 60 10000
# 2. AOF 开关 + 刷盘策略
appendonly yes
appendfsync everysec
# 3. 混合持久化
aof-use-rdb-preamble yes
# 4. 文件名 + 路径
appendfilename "appendonly.aof"
dir /var/lib/redisJava 端配置(application.yml 配连接):
spring:
redis:
host: 127.0.0.1
port: 6379
password: secret选 StringRedisTemplate 而不是 RedisTemplate:大多数业务只存字符串 / 数字,直接用 Spring Boot 默认提供的,不用写 @Configuration。
@Service
public class UserService {
// ① 直接注入(Spring Boot 自动配置,不用写 Bean)
@Autowired
private StringRedisTemplate redis;
// ② 存字符串
public void saveName(Long id, String name) {
redis.opsForValue().set("user:" + id + ":name", name);
}
// ③ 取字符串
public String getName(Long id) {
return redis.opsForValue().get("user:" + id + ":name");
}
// ④ 计数(自增)
public Long incrViews(Long articleId) {
return redis.opsForValue().increment("article:" + articleId + ":views");
}
// ⑤ 带过期时间
public void saveCode(String phone, String code) {
redis.opsForValue().set("code:" + phone, code, Duration.ofSeconds(60)); // 60 秒过期
}
// ⑥ 删
public void del(Long id) {
redis.delete("user:" + id + ":name");
}
}两个 Template 对比(选型用):
StringRedisTemplate | RedisTemplate<Object,Object> | |
|---|---|---|
| Key/Value 类型 | String | 任意(可存对象) |
| 序列化 | String(原样) | JDK 二进制(乱码,不可读) |
| 配置 | 不用写 | 要配 Jackson2JsonRedisSerializer |
| 适合 | 字符串/数字(主流) | 复杂对象(需要 JSON) |
实战选型口诀:
业务只存字符串、数字 → StringRedisTemplate(默认,够用)
业务要存 User/Order 等对象 → RedisTemplate + Jackson(JSON 序列化)要存对象(配 JSON 序列化):
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> tpl = new RedisTemplate<>();
tpl.setConnectionFactory(factory); // ① 连 Redis
// ② key 用 String("user:1" 这种)
tpl.setKeySerializer(new StringRedisSerializer());
// ③ value 用 JSON(对象→{"name":"tom"})
tpl.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
// ④ hash 字段也用 String
tpl.setHashKeySerializer(new StringRedisSerializer());
// ⑤ hash 字段值用 JSON
tpl.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
tpl.afterPropertiesSet(); // ⑥ 必须调,初始化
return tpl;
}
}Java 触发持久化命令(运维场景,生产一般用 redis-cli):
@Service
public class PersistenceService {
@Autowired
private StringRedisTemplate redis;
// ① 手动拍快照(BGSAVE)
public void takeSnapshot() {
redis.execute((RedisConnection conn) -> {
conn.bgsave(); // 触发 BGSAVE
return null;
});
}
// ② 查上次快照时间
public long lastSave() {
return redis.execute((RedisConnection conn) -> {
return conn.lastSave(); // 返回时间戳(秒)
});
}
// ③ 手动触发 AOF 重写
public void rewriteAof() {
redis.execute((RedisConnection conn) -> {
conn.bgRewriteAof(); // BGREWRITEAOF
return null;
});
}
// ④ 看持久化状态
public String persistenceInfo() {
return redis.execute((RedisConnection conn) -> {
Properties info = conn.info("persistence"); // INFO persistence
return info.toString();
});
}
}实战一般不用 Java 触发持久化,运维层面 redis-cli BGSAVE 更直观。
七、监控持久化状态
INFO persistence # 看 RDB/AOF 状态
LASTSAVE # 上次 RDB 成功时间(秒级时间戳)⚠️ 坑 3:不要相信"Redis 是内存数据库不需要持久化" —— 主从切换的瞬间、从节点首次同步时,没持久化就意味着丢全部历史数据。
八、备份与恢复
# 备份
cp /var/lib/redis/dump.rdb /backup/redis-2024-01-01.rdb
# 恢复:把 RDB 文件放到 Redis 数据目录,重启 Redis
docker run -v ./dump.rdb:/data/dump.rdb redis:7-alpine⚠️ 坑 4:恢复时 Redis 必须是干净状态,数据目录里不能同时有 RDB 和 AOF(AOF 优先级高,RDB 会被忽略)。
关键区分:混合持久化 vs 两个独立文件:
混合持久化(aof-use-rdb-preamble yes):
/data/appendonly.aof ← 一个文件,内部两种格式
┌──────────────────┬──────────────┐
│ RDB 格式(全量) │ AOF 格式(增量)│
└──────────────────┴──────────────┘
→ Redis 自动管理
坑 4 的"两个文件":
/data/dump.rdb ← 独立 RDB 备份
/data/appendonly.aof ← 独立 AOF 日志
→ AOF 优先, RDB 被忽略为什么 AOF 优先?
RDB = 某一刻的快照(12:00)
AOF = 12:00 ~ 12:05 所有命令
→ AOF 数据更全,所以优先用 AOF 恢复实操恢复(RDB 备份):
# 1. 关 Redis
redis-cli SHUTDOWN
# 2. 删除 AOF(关键!)
rm -f /var/lib/redis/appendonly.aof
# 3. 放 RDB 备份
cp /backup/redis-2024-01-01.rdb /var/lib/redis/dump.rdb
# 4. 启动
redis-server /etc/redis.conf
# → 只用 RDB 恢复(没 AOF 干扰)为什么 docker 挂载容易踩?
docker run -v ./dump.rdb:/data/dump.rdb redis:7-alpine
# ↑ 但镜像里 /data/ 已经有 appendonly.aof(镜像层带的)
# → 容器里两个文件都有
# → RDB 被忽略,备份"白备了"
# 解决:先起临时容器,再放数据
docker run --rm -d --name temp-redis redis:7-alpine
docker exec temp-redis redis-cli SHUTDOWN NOSAVE
docker run -v ./dump.rdb:/data/dump.rdb redis:7-alpine记忆口诀:
混合持久化 = 一个文件内部两种格式 ✅
备份恢复 = RDB 和 AOF 不能同时存在 ❌九、本章小结
| 要点 | 关键 |
|---|---|
| RDB | 定时快照,文件小、可能丢数据 |
| AOF | 追加日志,最多丢 1 秒 |
| 混合持久化 | Redis 4.0+ 推荐,兼得两者 |
| 配置 | appendonly yes + aof-use-rdb-preamble yes |
everysec | 生产默认刷盘策略 |
| 备份 | 定期拷贝 RDB 文件到异地 |
动手练习
- 启动 Redis,执行
CONFIG SET save ""关闭 RDB,然后BGSAVE,观察 dump.rdb 生成 - 开启 AOF(
CONFIG SET appendonly yes),写入数据,cat appendonly.aof看命令格式 - 模拟崩溃:
SHUTDOWN NOSAVE后重启 Redis,观察数据是否还在
下一章:第 4 章:Redis 集群 →