Lesson 63 · 数据库与中间件实战
Redis 持久化与高可用:RDB、AOF 与 Cluster 集群
Redis 持久化与高可用:RDB、AOF 与 Cluster 集群
RDB 快照 vs AOF 追加日志、混合持久化。哨兵模式 vs Cluster 分片集群。Redis 单线程为什么这么快?
Redis 单线程为什么这么快?
很多人疑惑:Redis 是单线程的,为什么还能达到 10 万+ QPS?答案是:瓶颈不在 CPU,在网络和内存。
Redis 6.0 引入了多线程,是怎么回事?
Redis 6.0 的多线程只用于网络 I/O(读写 socket、解析协议),命令执行仍然是单线程的。这是为了解决网络 I/O 成为瓶颈的问题(高并发时网络数据拷贝占用了大量 CPU)。所以"Redis 命令执行是单线程的"这个结论依然成立。
Redis 快的本质:内存操作(ns级)+ epoll 多路复用(高并发)+ 单线程(无锁开销)。单线程不是"慢"的原因——瓶颈在网络/内存,不在 CPU。
RDB 快照——某一时刻的全量内存镜像
RDB(Redis Database)是 Redis 的默认持久化方式:在指定时间间隔内将内存中的全部数据快照写入磁盘的 .rdb 文件。
# redis.conf save 900 1 # 900 秒内至少 1 次写操作 → 触发 BGSAVE save 300 10 # 300 秒内至少 10 次写操作 save 60 10000 # 60 秒内至少 10000 次写操作 # 手动触发 SAVE # 阻塞主进程(生产慎用) BGSAVE # fork 子进程(推荐)
| 优点 | 缺点 |
|---|---|
| 文件紧凑,恢复速度快 | 两次快照之间的数据可能丢失 |
| fork 子进程不影响主进程服务 | fork 大内存进程时可能阻塞毫秒级 |
| 适合备份和灾难恢复 | 频繁 fork 有 CPU 和内存开销 |
AOF 追加日志——记录每一条写命令
AOF(Append Only File)记录的是每一条写命令(SET/DEL/HSET 等),以 Redis 协议格式追加到 .aof 文件末尾。
# appendonly.aof 文件内容示例 *2 $6 SELECT $1 0 *3 $3 SET $3 foo $3 bar *2 $3 DEL $3 foo # 恢复时重放这些命令 → 还原数据
AOF 文件越来越大怎么办?
Redis 提供了 AOF 重写(BGREWRITEAOF)机制:fork 子进程,根据当前内存数据生成"最小等价命令集",替换旧的 AOF 文件。例如对 key "x" 执行了 100 次 SET,重写后只保留最后一次。重写过程中新产生的命令会同时写入 AOF 重写缓冲区,完成后合并。
| 对比维度 | RDB | AOF |
|---|---|---|
| 数据安全性 | 可能丢失两次快照间的数据 | 取决于 fsync 策略(everysec 最多丢 1 秒) |
| 文件大小 | 紧凑(二进制压缩) | 较大(文本格式命令) |
| 恢复速度 | 快(直接加载二进制) | 慢(需重放所有命令) |
| 写入性能 | fork 时有瞬间阻塞 | fsync 有持续 IO 开销 |
| 适用场景 | 备份、灾难恢复、冷备 | 数据安全要求高的生产环境 |
| 推荐策略 | 每小时一次定时备份 | always/everysec + 自动重写 |
# 自动触发 AOF 重写的条件 auto-aof-rewrite-percentage 100 # AOF 文件增长 100% 时触发 auto-aof-rewrite-min-size 64mb # 最小 64MB 才触发(避免频繁重写小文件) # 手动触发 BGREWRITEAOF # 查看 AOF 状态 INFO persistence # aof_enabled:1 # aof_rewrite_in_progress:0 # aof_current_size:104857600 (当前 AOF 文件大小) # aof_base_size:52428800 (上次重写后的大小)
混合持久化(Redis 4.0+)
Redis 4.0 引入了混合持久化:AOF 重写时,先以 RDB 格式写入全量数据,再追加增量 AOF 命令。兼顾了 RDB 的快速恢复和 AOF 的数据安全。
# redis.conf aof-use-rdb-preamble yes # Redis 5.0 默认开启 # 同时建议开启 AOF + RDB 双持久化 appendonly yes save 900 1 save 300 10 # 恢复优先级:AOF > RDB # 如果 AOF 存在,Redis 优先加载 AOF;AOF 不存在才加载 RDB
生产环境最佳实践:RDB + AOF(混合持久化)同时开启。RDB 用于快速备份和恢复,AOF 保证数据不丢。Redis 4.0+ 的混合持久化让 AOF 重写后体积更小、恢复更快。
Sentinel 哨兵——自动故障转移
Redis 主从复制(Master-Slave)解决了数据冗余,但主节点宕机时需要手动切换。Sentinel 实现了自动故障检测和故障转移。
1. 监控:Sentinel 每秒向 Master/Slave 发送 PING 2. 主观下线(SDOWN):某个 Sentinel 超时未收到回复 → 认为该节点下线 3. 客观下线(ODOWN):quorum 个 Sentinel 都同意 → 确认 Master 下线 4. 选举 Leader Sentinel:Raft 算法选出一个 Sentinel 执行故障转移 5. 选新 Master:从 Slave 中选一个(优先级 > 复制偏移量 > runid) 6. 切换:其他 Slave 复制新 Master,旧 Master 恢复后变 Slave 7. 通知:客户端通过 Sentinel 获取新 Master 地址
# sentinel.conf port 26379 # 监控的 Master 名称、地址、quorum(最少几个 Sentinel 同意才算下线) sentinel monitor mymaster 192.168.1.100 6379 2 # 主观下线超时(毫秒):超过这个时间没回复就认为下线 sentinel down-after-milliseconds mymaster 5000 # 故障转移超时(毫秒):整个故障转移过程的最大时间 sentinel failover-timeout mymaster 60000 # 故障转移时最多几个 Slave 同时切换 Master sentinel parallel-syncs mymaster 1
// Jedis Sentinel 示例 Set<String> sentinels = new HashSet<>(); sentinels.add("192.168.1.1:26379"); sentinels.add("192.168.1.2:26379"); sentinels.add("192.168.1.3:26379"); JedisSentinelPool pool = new JedisSentinelPool( "mymaster", // 监控的 Master 名称 sentinels, // Sentinel 节点列表 jedisPoolConfig, // 连接池配置 "password" // Redis 密码 ); // 自动获取当前 Master 连接(故障转移后自动切换) try (Jedis jedis = pool.getResource()) { jedis.set("key", "value"); }
Sentinel 模式有什么局限性?
Sentinel 解决了高可用问题,但没有解决数据分片问题——所有数据都存在一台 Master 上,内存上限受限于单机。写操作也无法并行。要水平扩展就需要 Cluster 集群。
Cluster 分片集群——数据分片 + 高可用
Redis Cluster 是官方提供的分布式解决方案:数据按 slot 分片存储到多个 Master 节点,每个 Master 可以有 Slave 做高可用。
客户端写入 key="user:1001" 1. 计算 slot = CRC16("user:1001") % 16384 = 7842 2. slot 7842 属于 Master-B(5461~10922) 3. 如果客户端连的是 Master-A → 收到 MOVED 7842 192.168.1.2:6379 4. 客户端重定向到 Master-B 执行命令 # 批量操作的 key 必须落在同一 slot → 使用 hash tag SET {user}:1001:name "Alice" SET {user}:1001:age 25 # {} 内的部分参与 CRC16 计算 → 保证同 slot
Cluster 将 16384 个 slot 分给多个 Master,key 通过 CRC16 定位 slot。最少 3 主 3 从部署。支持自动故障转移(类似 Sentinel 但内置于 Cluster)。不适合跨 slot 的批量操作(需要 hash tag 或 Lua 脚本)。
高可用架构选型总结
| 架构 | 数据分片 | 高可用 | 适用场景 | 复杂度 |
|---|---|---|---|---|
| 单机 | 无 | 无 | 开发测试 | 低 |
| 主从复制 | 无 | 手动切换 | 读多写少 | 低 |
| Sentinel 哨兵 | 无 | 自动故障转移 | 数据量不大,需高可用 | 中 |
| Cluster 集群 | 16384 slot | 自动故障转移 | 大数据量,高并发读写 | 高 |
| 淘汰策略 | 淘汰范围 | 淘汰算法 | 适用场景 |
|---|---|---|---|
| volatile-lru | 设置了 TTL 的 key | 最近最少使用 | 缓存场景(推荐) |
| allkeys-lru | 所有 key | 最近最少使用 | 纯缓存,所有 key 都可淘汰 |
| volatile-lfu | 设置了 TTL 的 key | 最不经常使用 | Redis 4.0+,热点数据场景 |
| allkeys-lfu | 所有 key | 最不经常使用 | Redis 4.0+,访问频率差异大 |
| volatile-random | 设置了 TTL 的 key | 随机 | 对淘汰无特殊要求 |
| volatile-ttl | 设置了 TTL 的 key | TTL 最短的优先 | 快过期的先淘汰 |
| noeviction | 不淘汰 | — | 内存满时拒绝写入(默认) |
# 慢查询配置 slowlog-log-slower-than 10000 # 超过 10ms 记录(微秒) slowlog-max-len 128 # 最多保留 128 条慢查询 # 查看慢查询 SLOWLOG GET 10 # 最近 10 条慢查询 SLOWLOG LEN # 慢查询总数 # 常见慢查询原因及优化: # 1. KEYS * → 改用 SCAN(渐进式遍历,不阻塞) # 2. HGETALL 大 Hash → 改用 HMGET 取需要的 field # 3. SMEMBERS 大 Set → 改用 SSCAN # 4. 大 Key 操作 → 拆分成多个小 Key # 5. FLUSHALL/FLUSHDB → 改用 UNLINK(异步删除,Redis 4.0+)
全篇回顾
- 单线程快的原因:内存操作 + epoll 多路复用 + 无锁竞争(Redis 6.0 多线程只用于网络 IO)
- RDB:fork 子进程写快照,恢复快但可能丢数据(两次快照之间的写入)
- AOF:追加写命令,数据安全但文件大、恢复慢。everysec 是最佳策略
- 混合持久化:AOF 重写时 RDB + AOF 增量,兼具两者优点
- Sentinel:自动故障转移,但不支持数据分片
- Cluster:16384 slot 分片 + 自动故障转移,适合大数据量场景
- 选型:小数据 Sentinel,大数据 Cluster,持久化一律开 RDB + AOF