Redis 持久化与高可用——AOF、RDB、混合持久化的选型与调优
Redis 持久化与高可用——AOF、RDB、混合持久化的选型与调优
适读人群:在生产中使用 Redis 的后端工程师和运维工程师 | 阅读时长:约17分钟 | 核心价值:搞清楚 Redis 持久化的工作原理,做出正确的高可用选型决策
一次让团队心惊肉跳的 Redis 数据丢失
2021年某天凌晨,我接到一个紧急电话。对方是一家做积分商城的公司,他们的 Redis 服务器因为磁盘满了宕机了。重启 Redis 后,用户的购物车数据全丢了——购物车就存在 Redis 里,而且没有开启任何持久化。
"我们以为 Redis 重启数据还在的……"
这种认知误区在很多团队都存在。Redis 默认不开启 AOF,而 RDB 的默认保存策略(15 分钟内有 1 次写操作就保存)在低频写入场景下几乎等于不保存。
这次事故之后,他们花了一周时间补救:开启了 AOF 持久化,搭建了哨兵集群,设置了磁盘用量告警。但如果事前就理解持久化机制,这些问题完全可以避免。
今天,把 Redis 的 RDB、AOF、混合持久化,以及哨兵和集群高可用方案,完整梳理一遍。
一、RDB 持久化:全量快照
1.1 工作原理
RDB(Redis Database Backup)通过生成某一时刻的全量数据快照来实现持久化。
触发 RDB 保存有两种方式:
- BGSAVE(推荐):Redis 主进程
fork()出子进程,子进程将内存数据写入临时 RDB 文件,写完后替换旧文件。主进程继续处理请求,不阻塞(但 fork 本身会短暂阻塞) - SAVE(不推荐):主进程直接写文件,期间所有请求被阻塞
# RDB 配置(redis.conf)
save 900 1 # 900 秒内有 1 次写操作,触发 BGSAVE
save 300 10 # 300 秒内有 10 次写操作,触发 BGSAVE
save 60 10000 # 60 秒内有 10000 次写操作,触发 BGSAVE
dbfilename dump.rdb # RDB 文件名
dir /var/lib/redis # 文件保存目录
# 关闭 RDB(只用 AOF 时)
save ""1.2 RDB 的 fork 开销
RDB 的核心痛点是 fork()。fork 时,Linux 内核需要复制父进程的页表(Page Table),页表大小与父进程内存大小成正比。如果 Redis 实例占用内存 32GB,fork 可能需要 300-500ms,这期间主进程阻塞,所有请求等待。
踩坑一:Redis 实例内存过大导致 BGSAVE 时延迟飙升
现象:每隔几分钟,Redis 的响应时间突然从 1ms 飙到 500ms,然后恢复正常。
原因:BGSAVE 触发,fork 耗时约 300ms,fork 期间主进程阻塞,所有请求排队。
解法:
- 减小单个 Redis 实例的内存(控制在 8GB 以内),fork 速度更快
- 关闭透明大页(Transparent HugePage):
echo never > /sys/kernel/mm/transparent_hugepage/enabled - 对实时性要求高的场景,关闭 RDB,只用 AOF
二、AOF 持久化:命令追加日志
2.1 工作原理
AOF(Append Only File)记录每一条写命令(SET/LPUSH/ZADD 等)到 AOF 文件,重启时通过重放 AOF 文件恢复数据。
# AOF 配置
appendonly yes # 开启 AOF
appendfilename "appendonly.aof"
# 三种 fsync 策略:
appendfsync always # 每条命令都立刻 fsync(最安全,但性能最差,不推荐)
appendfsync everysec # 每秒 fsync 一次(推荐,最多丢失 1 秒数据)
appendfsync no # 由操作系统决定 fsync 时机(性能最好,但可能丢失较多数据)
# AOF 重写配置
auto-aof-rewrite-percentage 100 # AOF 文件大小比上次重写后增长了 100%,触发重写
auto-aof-rewrite-min-size 64mb # AOF 文件至少 64MB 才触发重写(避免小文件频繁重写)2.2 AOF 重写:防止日志无限增长
随着写操作积累,AOF 文件会不断增大。AOF 重写(BGREWRITEAOF)会把当前内存状态"压缩"为最小的命令集合,替换旧的 AOF 文件。
例如:对同一个 key 执行了 100 次 SET,AOF 重写后只保留最后一次 SET,前 99 次都被消除。
// 监控 AOF 重写状态
@Component
public class RedisHealthMonitor {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Scheduled(fixedRate = 60000) // 每分钟检查一次
public void checkAofRewriteStatus() {
Properties info = (Properties) redisTemplate.execute(
(RedisCallback<Object>) conn -> conn.serverCommands().info("persistence"));
if (info != null) {
String aofRewriteInProgress = info.getProperty("aof_rewrite_in_progress");
String aofLastRewriteTime = info.getProperty("aof_last_rewrite_time_sec");
String aofCurrentSize = info.getProperty("aof_current_size");
log.info("AOF状态: 重写中={}, 上次重写耗时={}s, 当前大小={}bytes",
aofRewriteInProgress, aofLastRewriteTime, aofCurrentSize);
// 如果重写耗时超过 30 秒,可能是内存数据量太大
if (aofLastRewriteTime != null && Long.parseLong(aofLastRewriteTime) > 30) {
log.warn("AOF 重写耗时过长,考虑优化 Redis 内存使用");
}
}
}
}踩坑二:AOF 文件损坏导致 Redis 无法启动
现象:系统意外宕机(断电、内核 OOM Kill)后,Redis 重启时报 Bad file format reading the append only file。
原因:AOF 文件写到一半时系统崩溃,文件末尾有不完整的命令。
解法:
# 使用 Redis 提供的 AOF 修复工具
redis-check-aof --fix appendonly.aof
# 修复后,再次尝试启动 Redis
redis-server /etc/redis/redis.conf三、混合持久化:兼顾速度和安全
3.1 为什么需要混合持久化
- RDB:恢复速度快(直接加载内存快照),但最多丢失几分钟数据
- AOF:数据安全性高(最多丢失 1 秒),但恢复速度慢(需要重放所有命令)
混合持久化(Redis 4.0+):在 AOF 重写时,把当前时刻的 RDB 快照写入 AOF 文件头部,后面再追加这段时间内的增量 AOF 命令。
恢复时:先加载 RDB 快照(快),再重放增量 AOF(量少,速度可接受)。兼顾了恢复速度和数据安全性。
# 开启混合持久化(需要同时开启 AOF)
appendonly yes
aof-use-rdb-preamble yes # 开启混合持久化(Redis 7.0+ 默认开启)3.2 持久化选型建议
场景 推荐方案
---- --------
缓存(数据丢了可以重建) 不开持久化(最简单,性能最好)
Session/购物车等会话数据 AOF(everysec)
排行榜/计数器等重要数据 混合持久化
金融/支付相关(核心数据) Redis 不适合,改用数据库 + Redis 缓存四、高可用方案选型
4.1 哨兵模式(Sentinel)
适合中小规模,自动主从切换:
┌─────────────┐
│ Sentinel 1 │
└─────────────┘
│
┌────────────────────┼───────────────────┐
│ │ │
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Master │─────▶│ Slave 1 │ │ Slave 2 │
└─────────┘ └─────────┘ └─────────┘// Spring Boot 配置哨兵
@Configuration
public class RedisSentinelConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster") // 主节点名称,与 sentinel.conf 一致
.sentinel("sentinel-1", 26379)
.sentinel("sentinel-2", 26379)
.sentinel("sentinel-3", 26379);
sentinelConfig.setPassword(RedisPassword.of("your_password"));
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED) // 优先从从节点读
.build();
return new LettuceConnectionFactory(sentinelConfig, clientConfig);
}
}4.2 Cluster 集群模式
数据量超过单机内存限制时,用 Cluster 做水平分片:
- 16384 个 slot,数据按 key 的哈希值(
CRC16(key) % 16384)路由到对应 slot - 每个节点负责一段 slot 范围
- 支持自动故障转移
踩坑三:Cluster 模式下批量操作报错
现象:从单机迁移到 Cluster 后,大量 MGET、Pipeline 操作报错:CROSSSLOT Keys in request don't hash to the same slot。
原因:Cluster 要求同一命令的所有 key 必须在同一个 slot。MGET 多个不同 key,它们可能分布在不同节点,Cluster 不支持跨节点的批量操作。
解法:使用 Hash Tag 强制多个 key 落在同一 slot:
// 使用 Hash Tag:{userId} 部分相同,所有 key 落在同一 slot
String cartKey = "{user:" + userId + "}:cart";
String sessionKey = "{user:" + userId + "}:session";
String profileKey = "{user:" + userId + "}:profile";
// 这三个 key 的 slot 由 {user:12345} 这部分决定,保证落在同一 slot
// 可以对这三个 key 用 Pipeline 或 MGETRedis 的高可用不是一次配置好就万事大吉的,需要定期检查:持久化是否正常运行、AOF 文件大小是否在可控范围、主从同步是否有延迟、哨兵节点是否正常投票。把这些纳入常规巡检,Redis 才能真正可靠。
