RDB 持久化
触发配置
save 900 1 # 900s 内有 1 次更新
save 300 10 # 300s 内有 10 次更新
save 60 10000 # 60s 内有 10000 次更新
三个条件任一满足则触发 RDB 持久化。要禁用 RDB:
save ""
其他相关配置
stop-writes-on-bgsave-error yes
# 备份出错时主进程停止接受新写入,保护持久化数据一致性
rdbcompression yes
# 备份时压缩再保存,节约磁盘 IO;CPU 紧张可设为 no
手动触发
save:在主进程中创建快照,阻塞 Redis 服务直到 RDB 文件创建完毕,很少使用。BGSAVE:Fork 子进程创建 RDB 文件,不阻塞主进程。可结合定时器定期调用bgsave,用lastsave记录时间戳,形成多时间点的全量备份。
Fork & CopyOnWrite 原理
调用 bgsave 时:
- 检查是否有正在执行的 aof/rdb 进程,有则抛错。
- Fork 主进程创建子进程。fork 使用 CopyOnWrite(写时复制):
- 初始时父子进程指向同一块内存空间(共享页表)。
- 任意一方试图修改数据时,操作系统才真正复制那一页给修改方。
- 子进程把内存中的数据序列化为 RDB 文件。
rdb 的代价:全量同步,数据量大时 IO 严重影响性能。fork 阻塞时间与内存大小正相关(页表复制成本)。
检查持久化状态
lastsave # 上次持久化成功的时间戳
AOF 持久化
记录除查询以外所有变更数据库状态的指令,以 append 形式追加到 aof 文件,默认关闭。
开启
# redis.conf
appendonly yes
# 或运行时
config set appendonly yes
config rewrite
三种刷盘策略
appendfsync [always | everysec | no]
| 策略 | 行为 | 数据安全性 | 性能开销 |
|---|---|---|---|
always | 每次修改都 fsync | 不丢数据 | 高(一般 sata 盘只有几百 TPS) |
everysec | 每秒 fsync 一次 | 最多丢 1 秒数据 | 中(推荐) |
no | 交由 OS 控制 | 不可控 | 低 |
AOF 重写
随着时间推移,AOF 文件越来越大,但中间数据可以省略(多次 SET 同一 key、过期 key 失效等),通过重写精简。
两种触发方式:
- 手动:
BGREWRITEAOF - 自动:基于两个阈值
auto-aof-rewrite-min-size 64mb # AOF 文件达到这个大小开始重写
auto-aof-rewrite-percentage 100 # AOF 文件增长率(上次重写后翻倍触发)
为什么需要两个参数?单设尺寸阈值(如 64mb)的话,重写完成后数据增长超过 64mb 又会触发;加增长率默认 100%,第一次 64mb 触发后,第二次要 128mb 才触发。
重写原理:fork 子进程,子进程根据内存中的当前数据生成新 AOF 文件(不是基于原 AOF 处理),同时主进程继续将变更写入原 AOF 缓冲区,子进程完成后合并。
AOF 阻塞

设置 everysec 后,AOF 流程:
- 主线程将变更写入 AOF 缓冲区。
- 由同步线程每 1 秒把缓冲区 fsync 到磁盘。
- 主线程对比上次 fsync 时间,如果距离上次已超过 2 秒,主线程会阻塞等待同步线程刷盘完成。
- 阻塞期间所有新命令都会排队,造成 Redis 整体响应延迟。
监控指标:
INFO Persistence中的aof_delayed_fsync计数大于 0 就说明发生过 AOF 阻塞。解决办法:换 ssd 盘、降低写入压力、或开启no-appendfsync-on-rewrite yes。
RDB-AOF 混合模式(4.0+ 推荐)
aof-use-rdb-preamble yes
触发 AOF 重写后:
- 先以 RDB 格式保存全量数据。
- 再以 AOF 增量格式追加重写期间的新命令。
启动时如果同时存在 RDB 和 AOF 文件,优先加载 AOF。
对比总结:
- RDB:文件小、恢复快、但只能保留最近一次快照,容易丢数据。
- AOF:可读性高、数据安全(everysec 丢 1 秒)、但文件体积大、恢复慢。
- RDB-AOF 混合:兼顾两者优点,4.0 后默认推荐。
主从复制
配置从节点
# 命令方式
slaveof <主节点ip> <主节点端口>
# 配置文件
slaveof <ip> <port>
slave-read-only yes # 从节点只读(推荐)
slaveof <ip> <port>:成为从节点时清除从节点原有数据。slaveof no one:让当前从节点断开主从关系,数据保留,可继续作为其他主节点的从节点。
全量同步(首次同步)
从节点刚刚加入时的同步过程:
- slave 发送
SYNC命令到 master。 - master 启动后台进程,将 redis 中的数据快照保存到 RDB 文件。
- master 将保存快照期间接收到的写命令缓存起来。
- master 完成快照保存后,将 RDB 文件发送给 slave。
- slave 使用新 RDB 文件替换旧的 RDB 文件,恢复快照。
- master 将期间收集的增量写命令发送给 slave 进行回放。
Redis 2.8 之后用
PSYNC替代SYNC,支持部分重同步(基于repl_backlog复制偏移量),断线重连时只同步断开期间丢失的命令,不必每次都全量同步。
增量同步(运行期同步)
master 接收到用户写操作后的传播流程:
- 判断是否需要传播到 slave(一般增删改指令需要)。
- 将操作追加到 AOF 文件(如果开启)。
- 将操作传播到其他 slave:
- 对齐主从复制偏移量。
- 向响应缓存写入指令。
- 将缓存中的数据发送给 slave。
复制风暴
复制风暴:当主节点故障、新主节点产生时,所有从节点都要执行全量主从同步,fork 子进程、生成 RDB、在多个从节点间传输 RDB 都会消耗大量资源(CPU、磁盘 IO、网络带宽)。
解决:树状复制结构

让部分从节点挂在新主节点下作为二级主节点,二级从节点再挂在一级从节点下,避免所有从节点同时对单点拉取全量数据。
新的问题:当 slave-1 发生故障时,二级从节点全部要重新级联同步,层级越多同步链越长。生产中一般不超过 2 级。
Redis Sentinel(哨兵)
Sentinel 是 Redis 官方提供的高可用方案:自动监控、通知、故障迁移、配置提供。
架构


- 一主一从或一主多从作为数据节点。
- 多个 Sentinel 节点组成集群,互相通信监控数据节点。
- 客户端不再直连 Redis 实例,而是先连接 Sentinel 获取当前 master 地址,再连 master 进行操作。
Sentinel 自身也是一个不保存数据的 Redis 实例,多个部署(最少 3 个,部署在至少 2 台机器上)保证自身高可用。
故障迁移流程

故障迁移的关键步骤:
- 主观下线(SDOWN):单个 Sentinel 认为某个 master 不可达。
- 客观下线(ODOWN):超过
quorum(法定人数)数量的 Sentinel 都认为该 master 不可达,则客观下线。 - Leader 选举:在多个 Sentinel 中通过 Raft 简化版协议选举出一个 Leader 来执行 failover。
- 选新主:从 slave 中选出数据最新(
slave-priority、repl offset)的作为新 master。 - 切换角色:其他 slave 切换到复制新 master,原 master 恢复后变成新 master 的 slave。
Sentinel 选举机制(Raft 简化版)
- 每个 Sentinel 节点在发现 master 客观下线后,可以请求其他 Sentinel 给自己投票。
- 每个 Sentinel 在一轮投票中只能投一票,先到先得。
- 获得多数票(> Sentinel 总数 / 2)的 Sentinel 成为 Leader,执行故障迁移。
- 如果本轮没有选出 Leader,等一轮后重新投票(避免活锁)。
Sentinel 集群要求奇数个节点(3、5、7),因为偶数节点的多数票和少 1 个节点一样(如 4 个和 3 个都需要 3 票容灾能力反而更低)。
客户端集成

Jedis 客户端的实现原理:
- 启动时配置
masterName和sentinel地址集合。 - 连接 sentinel 获取当前可用 master。
- 连接 master 进行操作。
- 监听 sentinel 的 +switch-master 频道,当 master 切换时自动重连新 master。
// Jedis 示例
Set<String> sentinels = new HashSet<>();
sentinels.add(new HostAndPort("10.0.0.1", 26379).toString());
sentinels.add(new HostAndPort("10.0.0.2", 26379).toString());
JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels, config);
Jedis jedis = pool.getResource();
jedis.set("key", "value");