Redis持久化深度:RDB、AOF、混合持久化的性能与数据安全权衡
Redis持久化深度:RDB、AOF、混合持久化的性能与数据安全权衡
适读人群:中高级Java工程师 | 阅读时长:约18分钟 | 技术栈:Redis 7.x、Spring Boot 3.x
开篇故事
2020年,我们有一个 Redis 节点用来存储用户的 Session 信息。那时候年轻,觉得 Session 在内存里就够了,没有开启任何持久化。有一天,那台服务器的内存条松了(物理原因),服务器强制重启,Redis 数据全部丢失。
大约 4 万个用户的 Session 被清空,用户全部被强制退出登录。当天用户投诉量直接飙升,差评如潮。更麻烦的是,有一些用户当时正在填写表单(购物信息、收货地址),Session 丢失后表单数据也没了,用户体验极差。
从那以后,我们对 Redis 持久化这件事就非常重视了。但开了持久化之后,又踩了另一个坑:开了 AOF 的 appendfsync always 模式(每次写操作都 fsync),Redis 的写性能从 8 万 QPS 直接降到了 1.2 万 QPS,降幅将近 85%。
这两个极端之间,如何选择才是合理的?今天把 Redis 持久化的所有关键点说清楚。
一、核心问题分析
Redis 持久化解决的是同一个问题:Redis 重启后,如何恢复数据?
两种持久化机制的本质差异:
RDB(Redis Database):定期对内存中的完整数据做快照,保存到磁盘文件。好比给数据拍一张照片,恢复快,但照片之间有时间间隔,间隔期间的数据丢失。
AOF(Append Only File):记录每一次写操作命令,重启时重新执行所有命令来恢复数据。好比逐条记录日志,不丢数据(取决于配置),但恢复慢,日志文件比 RDB 大。
二、原理深度解析
RDB 的工作机制
Redis 执行 RDB 快照的方式是 fork() 一个子进程,由子进程负责将内存数据写入临时 RDB 文件,完成后替换旧的 RDB 文件。父进程继续处理客户端请求,通过 Copy-On-Write(写时复制)与子进程共享内存。
fork 的代价:fork 操作本身需要复制父进程的页表,内存越大,fork 耗时越长。Redis 内存 10GB 的节点,fork 可能需要几十毫秒,这段时间内 Redis 会出现明显延迟(主进程被 fork 系统调用阻塞)。
CoW 的代价:fork 之后,如果父进程的写操作很频繁(高写入 QPS),大量内存页需要复制(CoW),内存使用量会暴增,可能从 10GB 涨到 18GB,触发 OOM(内存溢出)。
AOF 的三种 fsync 策略
AOF 将写操作追加到日志缓冲区,根据配置决定何时将缓冲区刷入磁盘(fsync):
appendfsync always:每次写操作后立即 fsync。最安全(最多丢失 1 条命令),最慢(每次写操作都等磁盘 IO)。生产中几乎不用,除非对数据安全有极致要求。
appendfsync everysec:每秒 fsync 一次。最多丢失 1 秒数据,性能好(fsync 是后台线程),推荐配置。
appendfsync no:由操作系统决定何时 fsync(通常 30 秒)。最快,最多丢失 30 秒数据,有一定风险。
AOF 重写(Rewrite)
AOF 文件会随时间不断增长,Redis 提供了 AOF 重写机制:用当前内存中的数据,生成一个最精简的 AOF 文件(去掉无效命令,比如多次 SET 同一个 key 只保留最后一次)。
重写也是通过 fork 子进程实现的,与 RDB 类似。
混合持久化(Redis 4.0+)
混合持久化是 RDB + AOF 的结合体:AOF 重写时,先将当前内存状态以 RDB 格式写入 AOF 文件头部,再将重写过程中新增的操作以 AOF 命令格式追加。
这样重启恢复时:先用 RDB 格式快速加载大部分数据,再用 AOF 格式回放少量增量命令。兼顾了 RDB 的快速恢复和 AOF 的数据安全。
三、性能测试数据
我在一台 8核16G 的服务器上做了对比测试(纯写入场景,50万条 string 写入):
| 配置 | 写入吞吐量 | 平均延迟 | 数据安全级别 |
|---|---|---|---|
| 无持久化 | 82,000 QPS | 0.12ms | 无(重启全丢) |
| RDB only (每5分钟) | 78,000 QPS | 0.13ms | 低(最多丢5分钟) |
| AOF everysec | 73,000 QPS | 0.14ms | 中(最多丢1秒) |
| 混合持久化 everysec | 72,000 QPS | 0.14ms | 中(最多丢1秒) |
| AOF always | 11,000 QPS | 0.9ms | 高(最多丢1条) |
AOF everysec 相比无持久化只损失约 11% 的性能,而 AOF always 损失高达 87%。这就是开篇故事里从 8 万降到 1.2 万的原因。
四、完整配置实现
生产推荐配置(混合持久化)
# redis.conf
# ======== RDB 配置 ========
# RDB 快照规则:N秒内发生M次写操作则触发快照
save 3600 1 # 1小时内至少1次写操作
save 300 100 # 5分钟内至少100次写操作
save 60 10000 # 1分钟内至少10000次写操作
# RDB 文件名
dbfilename dump.rdb
dir /data/redis
# RDB 压缩(推荐开启,减少磁盘占用)
rdbcompression yes
# RDB 校验和(重启时校验文件完整性,有微小性能开销)
rdbchecksum yes
# ======== AOF 配置 ========
# 开启 AOF
appendonly yes
appendfilename "appendonly.aof"
# fsync 策略:推荐 everysec
appendfsync everysec
# AOF 重写期间,是否暂停 fsync(避免 IO 竞争)
# no: 重写期间照常 fsync,最安全;yes: 重写期间不 fsync,性能好但最多丢 1 秒数据
no-appendfsync-on-rewrite no
# AOF 重写触发条件
auto-aof-rewrite-percentage 100 # AOF 文件比上次重写后的大小增长了100%
auto-aof-rewrite-min-size 64mb # 且文件大小超过 64MB 才触发重写
# ======== 混合持久化 ========
# 开启混合持久化(Redis 7.x 默认开启)
aof-use-rdb-preamble yesJava 监控 Redis 持久化状态
@Component
@Slf4j
public class RedisPersistenceMonitor {
@Autowired
private StringRedisTemplate redisTemplate;
@Scheduled(fixedRate = 60_000) // 每分钟检查一次
public void checkPersistenceStatus() {
try {
Properties info = redisTemplate.getConnectionFactory()
.getConnection().serverCommands().info("persistence");
if (info == null) {
return;
}
// 检查 RDB 状态
String rdbLastBgSaveStatus = info.getProperty("rdb_last_bgsave_status");
String rdbLastBgSaveTimeSec = info.getProperty("rdb_last_bgsave_time_sec");
long rdbChangesSinceLastSave = Long.parseLong(
info.getProperty("rdb_changes_since_last_save", "0"));
if ("err".equals(rdbLastBgSaveStatus)) {
log.error("RDB 快照失败!上次耗时={}s", rdbLastBgSaveTimeSec);
sendAlert("Redis RDB 快照失败");
}
// 如果距离上次快照超过10分钟且有大量变更,告警
long lastSaveTime = Long.parseLong(info.getProperty("rdb_last_save_time", "0"));
long minutesSinceLastSave = (System.currentTimeMillis() / 1000 - lastSaveTime) / 60;
if (minutesSinceLastSave > 10 && rdbChangesSinceLastSave > 10000) {
log.warn("RDB 快照超过10分钟未执行,距上次快照有{}条变更", rdbChangesSinceLastSave);
}
// 检查 AOF 状态
String aofEnabled = info.getProperty("aof_enabled");
if ("1".equals(aofEnabled)) {
String aofLastRewriteStatus = info.getProperty("aof_last_rewrite_status");
if ("err".equals(aofLastRewriteStatus)) {
log.error("AOF 重写失败!");
sendAlert("Redis AOF 重写失败");
}
// AOF 文件大小告警
long aofCurrentSize = Long.parseLong(
info.getProperty("aof_current_size", "0"));
long aofMaxSize = 5L * 1024 * 1024 * 1024; // 5GB 告警
if (aofCurrentSize > aofMaxSize) {
log.warn("AOF 文件过大:{}GB", aofCurrentSize / 1024 / 1024 / 1024);
}
}
log.debug("Redis 持久化状态检查完成:rdbChanges={}, aofEnabled={}",
rdbChangesSinceLastSave, aofEnabled);
} catch (Exception e) {
log.error("Redis 持久化状态检查失败", e);
}
}
private void sendAlert(String message) {
// 发送告警通知
log.error("ALERT: {}", message);
}
}触发手动快照
@Service
@Slf4j
public class RedisManagementService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 手动触发 RDB 快照(在系统维护或重启前执行)
*/
public void triggerBgSave() {
redisTemplate.execute((RedisCallback<Void>) connection -> {
connection.serverCommands().bgSave();
log.info("已触发 Redis BGSAVE");
return null;
});
}
/**
* 手动触发 AOF 重写
*/
public void triggerBgRewriteAof() {
redisTemplate.execute((RedisCallback<Void>) connection -> {
connection.serverCommands().bgReWriteAof();
log.info("已触发 Redis BGREWRITEAOF");
return null;
});
}
/**
* 查询持久化详情
*/
public Map<String, String> getPersistenceInfo() {
Properties info = redisTemplate.execute((RedisCallback<Properties>) connection ->
connection.serverCommands().info("persistence")
);
if (info == null) {
return Collections.emptyMap();
}
Map<String, String> result = new HashMap<>();
info.forEach((k, v) -> result.put(k.toString(), v.toString()));
return result;
}
}五、踩坑实录
坑一:开篇 AOF always 性能暴降
错误配置:appendfsync always,写 QPS 从 8 万降到 1.2 万。
根本原因:always 模式下,每个写命令都要等待磁盘 fsync 完成,HDD 的 fsync 延迟约 5-10ms,SSD 也需要 0.5-1ms,而 Redis 的正常响应在 0.1-0.3ms,fsync 完全成了瓶颈。
修复:改为 appendfsync everysec,写 QPS 恢复到 7.3 万,而数据安全从"丢1条"变为"丢1秒",对我们的业务(Session 存储)完全可以接受。
坑二:fork 期间的 CPU 尖刺
生产中某个 Redis 节点(内存 24GB),触发 RDB 快照时,主进程会有约 200ms 的停顿(fork 系统调用),期间所有请求都超时。监控显示每隔 5 分钟(我们的 save 规则之一)就有一次明显的 P99 尖刺。
解决方案:
- 调整 save 规则,减少 RDB 快照频率(改为每 15 分钟触发一次)。
- 主要依靠 AOF 保证数据安全,RDB 只用于灾难恢复。
- 将 Redis 内存从 24GB 降到 16GB(拆分数据到多个节点),减少 fork 耗时。
坑三:AOF 文件损坏导致 Redis 无法启动
有一次机器意外断电,AOF 文件的最后几行写入不完整(文件截断),Redis 重启时检测到 AOF 文件损坏,拒绝启动。
Redis 提供了修复工具:
# 修复损坏的 AOF 文件(会删除损坏的最后几条命令)
redis-check-aof --fix /data/redis/appendonly.aof修复后可以正常启动,但最后几条命令丢失了。教训:在 RDB + AOF 双开的情况下,如果 AOF 损坏,可以先临时关闭 AOF,让 Redis 从 RDB 恢复,再重新开启 AOF。
六、总结
Redis 持久化选型的实践原则:
纯缓存场景(数据可以从 DB 重建):可以只开 RDB 甚至不开持久化,重启后从 DB 预热即可。
Session 等需要持久化但允许少量丢失:开启 AOF everysec + RDB,推荐混合持久化模式。性能损失约 10%,最多丢失 1 秒数据。
不能丢失任何数据(如分布式锁的元数据):AOF always,性能代价巨大,建议评估是否有更合适的方案(比如 ZK 或数据库),而不是强撑 Redis。
最佳实践:AOF everysec + 混合持久化 + 定期备份 RDB 文件到对象存储(S3/OSS)。既保证了正常运行时的数据安全,又有灾难恢复的兜底手段。
